Completed
Push — master ( bd9947...50f301 )
by Carsten
23:39
created

Formatter::asText()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
ccs 4
cts 4
cp 1
cc 2
eloc 4
nc 2
nop 1
crap 2
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 DateInterval;
11
use DateTime;
12
use DateTimeInterface;
13
use DateTimeZone;
14
use IntlDateFormatter;
15
use NumberFormatter;
16
use Yii;
17
use yii\base\Component;
18
use yii\base\InvalidConfigException;
19
use yii\base\InvalidParamException;
20
use yii\helpers\FormatConverter;
21
use yii\helpers\HtmlPurifier;
22
use yii\helpers\Html;
23
24
/**
25
 * Formatter provides a set of commonly used data formatting methods.
26
 *
27
 * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
28
 * The behavior of some of them may be configured via the properties of Formatter. For example,
29
 * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
30
 *
31
 * Formatter is configured as an application component in [[\yii\base\Application]] by default.
32
 * You can access that instance via `Yii::$app->formatter`.
33
 *
34
 * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
35
 * the [PHP intl extension](http://php.net/manual/en/book.intl.php) has to be installed.
36
 * Most of the methods however work also if the PHP intl extension is not installed by providing
37
 * a fallback implementation. Without intl month and day names are in English only.
38
 * Note that even if the intl extension is installed, formatting date and time values for years >=2038 or <=1901
39
 * on 32bit systems will fall back to the PHP implementation because intl uses a 32bit UNIX timestamp internally.
40
 * On a 64bit system the intl formatter is used in all cases if installed.
41
 *
42
 * @author Qiang Xue <[email protected]>
43
 * @author Enrica Ruedin <[email protected]>
44
 * @author Carsten Brandt <[email protected]>
45
 * @since 2.0
46
 */
47
class Formatter extends Component
48
{
49
    /**
50
     * @var string the text to be displayed when formatting a `null` value.
51
     * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
52
     * will be translated according to [[locale]].
53
     */
54
    public $nullDisplay;
55
    /**
56
     * @var array the text to be displayed when formatting a boolean value. The first element corresponds
57
     * to the text displayed for `false`, the second element for `true`.
58
     * Defaults to `['No', 'Yes']`, where `Yes` and `No`
59
     * will be translated according to [[locale]].
60
     */
61
    public $booleanFormat;
62
    /**
63
     * @var string the locale ID that is used to localize the date and number formatting.
64
     * For number and date formatting this is only effective when the
65
     * [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
66
     * If not set, [[\yii\base\Application::language]] will be used.
67
     */
68
    public $locale;
69
    /**
70
     * @var string the time zone to use for formatting time and date values.
71
     *
72
     * This can be any value that may be passed to [date_default_timezone_set()](http://www.php.net/manual/en/function.date-default-timezone-set.php)
73
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
74
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
75
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
76
     *
77
     * 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.
78
     * If you store your data in a different time zone in the database, you have to adjust [[defaultTimeZone]] accordingly.
79
     */
80
    public $timeZone;
81
    /**
82
     * @var string the time zone that is assumed for input values if they do not include a time zone explicitly.
83
     *
84
     * The value must be a valid time zone identifier, e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
85
     * Please refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
86
     *
87
     * It defaults to `UTC` so you only have to adjust this value if you store datetime values in another time zone in your database.
88
     *
89
     * @since 2.0.1
90
     */
91
    public $defaultTimeZone = 'UTC';
92
    /**
93
     * @var string the default format string to be used to format a [[asDate()|date]].
94
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
95
     *
96
     * 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).
97
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
98
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
99
     *
100
     * For example:
101
     *
102
     * ```php
103
     * 'MM/dd/yyyy' // date in ICU format
104
     * 'php:m/d/Y' // the same date in PHP format
105
     * ```
106
     */
107
    public $dateFormat = 'medium';
108
    /**
109
     * @var string the default format string to be used to format a [[asTime()|time]].
110
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
111
     *
112
     * 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).
113
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
114
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
115
     *
116
     * For example:
117
     *
118
     * ```php
119
     * 'HH:mm:ss' // time in ICU format
120
     * 'php:H:i:s' // the same time in PHP format
121
     * ```
122
     */
123
    public $timeFormat = 'medium';
124
    /**
125
     * @var string the default format string to be used to format a [[asDatetime()|date and time]].
126
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
127
     *
128
     * 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).
129
     *
130
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
131
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
132
     *
133
     * For example:
134
     *
135
     * ```php
136
     * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
137
     * 'php:m/d/Y H:i:s' // the same date and time in PHP format
138
     * ```
139
     */
140
    public $datetimeFormat = 'medium';
141
    /**
142
     * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
143
     * passed to the [constructor of the `IntlDateFormatter` class](http://php.net/manual/en/intldateformatter.create.php).
144
     *
145
     * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
146
     * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
147
     *
148
     * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
149
     * set this property to `\IntlDateFormatter::TRADITIONAL`.
150
     * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
151
     *
152
     * ```php
153
     * 'formatter' => [
154
     *     'locale' => 'fa_IR@calendar=persian',
155
     *     'calendar' => \IntlDateFormatter::TRADITIONAL,
156
     * ],
157
     * ```
158
     *
159
     * Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar).
160
     *
161
     * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
162
     * Check the [PHP manual](http://php.net/manual/en/intldateformatter.create.php) for more details.
163
     *
164
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
165
     *
166
     * @see http://php.net/manual/en/intldateformatter.create.php
167
     * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
168
     * @see http://php.net/manual/en/class.intlcalendar.php
169
     * @since 2.0.7
170
     */
171
    public $calendar;
172
    /**
173
     * @var string the character displayed as the decimal point when formatting a number.
174
     * If not set, the decimal separator corresponding to [[locale]] will be used.
175
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'.
176
     */
177
    public $decimalSeparator;
178
    /**
179
     * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
180
     * If not set, the thousand separator corresponding to [[locale]] will be used.
181
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is ','.
182
     */
183
    public $thousandSeparator;
184
    /**
185
     * @var array a list of name value pairs that are passed to the
186
     * intl [Numberformatter::setAttribute()](http://php.net/manual/en/numberformatter.setattribute.php) method of all
187
     * the number formatter objects created by [[createNumberFormatter()]].
188
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
189
     *
190
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
191
     * for the possible options.
192
     *
193
     * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
194
     *
195
     * ```php
196
     * [
197
     *     NumberFormatter::MIN_FRACTION_DIGITS => 0,
198
     *     NumberFormatter::MAX_FRACTION_DIGITS => 2,
199
     * ]
200
     * ```
201
     */
202
    public $numberFormatterOptions = [];
203
    /**
204
     * @var array a list of name value pairs that are passed to the
205
     * intl [Numberformatter::setTextAttribute()](http://php.net/manual/en/numberformatter.settextattribute.php) method of all
206
     * the number formatter objects created by [[createNumberFormatter()]].
207
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
208
     *
209
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
210
     * for the possible options.
211
     *
212
     * For example to change the minus sign for negative numbers you can configure this property like the following:
213
     *
214
     * ```php
215
     * [
216
     *     NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
217
     * ]
218
     * ```
219
     */
220
    public $numberFormatterTextOptions = [];
221
    /**
222
     * @var array a list of name value pairs that are passed to the
223
     * intl [Numberformatter::setSymbol()](http://php.net/manual/en/numberformatter.setsymbol.php) method of all
224
     * the number formatter objects created by [[createNumberFormatter()]].
225
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
226
     *
227
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatsymbol)
228
     * for the possible options.
229
     *
230
     * For example to choose a custom currency symbol, e.g. [U+20BD](http://unicode-table.com/en/20BD/) instead of `руб.` for Russian Ruble:
231
     *
232
     * ```php
233
     * [
234
     *     NumberFormatter::CURRENCY_SYMBOL => '₽',
235
     * ]
236
     * ```
237
     *
238
     * @since 2.0.4
239
     */
240
    public $numberFormatterSymbols = [];
241
    /**
242
     * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
243
     * If not set, the currency code corresponding to [[locale]] will be used.
244
     * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
245
     * is not possible to determine the default currency.
246
     */
247
    public $currencyCode;
248
    /**
249
     * @var int the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
250
     * Defaults to 1024.
251
     */
252
    public $sizeFormatBase = 1024;
253
254
    /**
255
     * @var bool whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded.
256
     */
257
    private $_intlLoaded = false;
258
259
260
    /**
261
     * @inheritdoc
262
     */
263 237
    public function init()
264
    {
265 237
        if ($this->timeZone === null) {
266 237
            $this->timeZone = Yii::$app->timeZone;
267 237
        }
268 237
        if ($this->locale === null) {
269 25
            $this->locale = Yii::$app->language;
270 25
        }
271 237
        if ($this->booleanFormat === null) {
272 237
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
273 237
        }
274 237
        if ($this->nullDisplay === null) {
275 237
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
276 237
        }
277 237
        $this->_intlLoaded = extension_loaded('intl');
278 237
        if (!$this->_intlLoaded) {
279 115
            if ($this->decimalSeparator === null) {
280 115
                $this->decimalSeparator = '.';
281 115
            }
282 115
            if ($this->thousandSeparator === null) {
283 115
                $this->thousandSeparator = ',';
284 115
            }
285 115
        }
286 237
    }
287
288
    /**
289
     * Formats the value based on the given format type.
290
     * This method will call one of the "as" methods available in this class to do the formatting.
291
     * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
292
     * then [[asHtml()]] will be used. Format names are case insensitive.
293
     * @param mixed $value the value to be formatted.
294
     * @param string|array $format the format of the value, e.g., "html", "text". To specify additional
295
     * parameters of the formatting method, you may use an array. The first element of the array
296
     * specifies the format name, while the rest of the elements will be used as the parameters to the formatting
297
     * method. For example, a format of `['date', 'Y-m-d']` will cause the invocation of `asDate($value, 'Y-m-d')`.
298
     * @return string the formatting result.
299
     * @throws InvalidParamException if the format type is not supported by this class.
300
     */
301 10
    public function format($value, $format)
302
    {
303 10
        if (is_array($format)) {
304 7
            if (!isset($format[0])) {
305
                throw new InvalidParamException('The $format array must contain at least one element.');
306
            }
307 7
            $f = $format[0];
308 7
            $format[0] = $value;
309 7
            $params = $format;
310 7
            $format = $f;
311 7
        } else {
312 5
            $params = [$value];
313
        }
314 10
        $method = 'as' . $format;
315 10
        if ($this->hasMethod($method)) {
316 10
            return call_user_func_array([$this, $method], $params);
317
        } else {
318 2
            throw new InvalidParamException("Unknown format type: $format");
319
        }
320
    }
321
322
323
    // simple formats
324
325
326
    /**
327
     * Formats the value as is without any formatting.
328
     * This method simply returns back the parameter without any format.
329
     * The only exception is a `null` value which will be formatted using [[nullDisplay]].
330
     * @param mixed $value the value to be formatted.
331
     * @return string the formatted result.
332
     */
333 1
    public function asRaw($value)
334
    {
335 1
        if ($value === null) {
336 1
            return $this->nullDisplay;
337
        }
338 1
        return $value;
339
    }
340
341
    /**
342
     * Formats the value as an HTML-encoded plain text.
343
     * @param string $value the value to be formatted.
344
     * @return string the formatted result.
345
     */
346 4
    public function asText($value)
347
    {
348 4
        if ($value === null) {
349 2
            return $this->nullDisplay;
350
        }
351 4
        return Html::encode($value);
352
    }
353
354
    /**
355
     * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
356
     * @param string $value the value to be formatted.
357
     * @return string the formatted result.
358
     */
359 1
    public function asNtext($value)
360
    {
361 1
        if ($value === null) {
362 1
            return $this->nullDisplay;
363
        }
364 1
        return nl2br(Html::encode($value));
365
    }
366
367
    /**
368
     * Formats the value as HTML-encoded text paragraphs.
369
     * Each text paragraph is enclosed within a `<p>` tag.
370
     * One or multiple consecutive empty lines divide two paragraphs.
371
     * @param string $value the value to be formatted.
372
     * @return string the formatted result.
373
     */
374 1
    public function asParagraphs($value)
375
    {
376 1
        if ($value === null) {
377 1
            return $this->nullDisplay;
378
        }
379 1
        return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/u', "</p>\n<p>", Html::encode($value)) . '</p>');
380
    }
381
382
    /**
383
     * Formats the value as HTML text.
384
     * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
385
     * Use [[asRaw()]] if you do not want any purification of the value.
386
     * @param string $value the value to be formatted.
387
     * @param array|null $config the configuration for the HTMLPurifier class.
388
     * @return string the formatted result.
389
     */
390
    public function asHtml($value, $config = null)
391
    {
392
        if ($value === null) {
393
            return $this->nullDisplay;
394
        }
395
        return HtmlPurifier::process($value, $config);
396
    }
397
398
    /**
399
     * Formats the value as a mailto link.
400
     * @param string $value the value to be formatted.
401
     * @param array $options the tag options in terms of name-value pairs. See [[Html::mailto()]].
402
     * @return string the formatted result.
403
     */
404 1
    public function asEmail($value, $options = [])
405
    {
406 1
        if ($value === null) {
407 1
            return $this->nullDisplay;
408
        }
409 1
        return Html::mailto(Html::encode($value), $value, $options);
410
    }
411
412
    /**
413
     * Formats the value as an image tag.
414
     * @param mixed $value the value to be formatted.
415
     * @param array $options the tag options in terms of name-value pairs. See [[Html::img()]].
416
     * @return string the formatted result.
417
     */
418 1
    public function asImage($value, $options = [])
419
    {
420 1
        if ($value === null) {
421 1
            return $this->nullDisplay;
422
        }
423 1
        return Html::img($value, $options);
424
    }
425
426
    /**
427
     * Formats the value as a hyperlink.
428
     * @param mixed $value the value to be formatted.
429
     * @param array $options the tag options in terms of name-value pairs. See [[Html::a()]].
430
     * @return string the formatted result.
431
     */
432 1
    public function asUrl($value, $options = [])
433
    {
434 1
        if ($value === null) {
435 1
            return $this->nullDisplay;
436
        }
437 1
        $url = $value;
438 1
        if (strpos($url, '://') === false) {
439 1
            $url = 'http://' . $url;
440 1
        }
441
442 1
        return Html::a(Html::encode($value), $url, $options);
443
    }
444
445
    /**
446
     * Formats the value as a boolean.
447
     * @param mixed $value the value to be formatted.
448
     * @return string the formatted result.
449
     * @see booleanFormat
450
     */
451 1
    public function asBoolean($value)
452
    {
453 1
        if ($value === null) {
454 1
            return $this->nullDisplay;
455
        }
456
457 1
        return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
458
    }
459
460
461
    // date and time formats
462
463
464
    /**
465
     * Formats the value as a date.
466
     * @param int|string|DateTime $value the value to be formatted. The following
467
     * types of value are supported:
468
     *
469
     * - an integer representing a UNIX timestamp
470
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
471
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
472
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
473
     *
474
     * @param string $format the format used to convert the value into a date string.
475
     * If null, [[dateFormat]] will be used.
476
     *
477
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
478
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
479
     *
480
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
481
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
482
     *
483
     * @return string the formatted result.
484
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
485
     * @throws InvalidConfigException if the date format is invalid.
486
     * @see dateFormat
487
     */
488 163
    public function asDate($value, $format = null)
489
    {
490 163
        if ($format === null) {
491 145
            $format = $this->dateFormat;
492 145
        }
493 163
        return $this->formatDateTimeValue($value, $format, 'date');
494
    }
495
496
    /**
497
     * Formats the value as a time.
498
     * @param int|string|DateTime $value the value to be formatted. The following
499
     * types of value are supported:
500
     *
501
     * - an integer representing a UNIX timestamp
502
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
503
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
504
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
505
     *
506
     * @param string $format the format used to convert the value into a date string.
507
     * If null, [[timeFormat]] will be used.
508
     *
509
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
510
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
511
     *
512
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
513
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
514
     *
515
     * @return string the formatted result.
516
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
517
     * @throws InvalidConfigException if the date format is invalid.
518
     * @see timeFormat
519
     */
520 145
    public function asTime($value, $format = null)
521
    {
522 145
        if ($format === null) {
523 143
            $format = $this->timeFormat;
524 143
        }
525 145
        return $this->formatDateTimeValue($value, $format, 'time');
526
    }
527
528
    /**
529
     * Formats the value as a datetime.
530
     * @param int|string|DateTime $value the value to be formatted. The following
531
     * types of value are supported:
532
     *
533
     * - an integer representing a UNIX timestamp
534
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
535
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
536
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
537
     *
538
     * @param string $format the format used to convert the value into a date string.
539
     * If null, [[dateFormat]] will be used.
540
     *
541
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
542
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
543
     *
544
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
545
     * PHP [date()](http://php.net/manual/en/function.date.php)-function.
546
     *
547
     * @return string the formatted result.
548
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
549
     * @throws InvalidConfigException if the date format is invalid.
550
     * @see datetimeFormat
551
     */
552 148
    public function asDatetime($value, $format = null)
553
    {
554 148
        if ($format === null) {
555 143
            $format = $this->datetimeFormat;
556 143
        }
557 148
        return $this->formatDateTimeValue($value, $format, 'datetime');
558
    }
559
560
    /**
561
     * @var array map of short format names to IntlDateFormatter constant values.
562
     */
563
    private $_dateFormats = [
564
        'short'  => 3, // IntlDateFormatter::SHORT,
565
        'medium' => 2, // IntlDateFormatter::MEDIUM,
566
        'long'   => 1, // IntlDateFormatter::LONG,
567
        'full'   => 0, // IntlDateFormatter::FULL,
568
    ];
569
570
    /**
571
     * @param int|string|DateTime $value the value to be formatted. The following
572
     * types of value are supported:
573
     *
574
     * - an integer representing a UNIX timestamp
575
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
576
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
577
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
578
     *
579
     * @param string $format the format used to convert the value into a date string.
580
     * @param string $type 'date', 'time', or 'datetime'.
581
     * @throws InvalidConfigException if the date format is invalid.
582
     * @return string the formatted result.
583
     */
584 170
    private function formatDateTimeValue($value, $format, $type)
585
    {
586 170
        $timeZone = $this->timeZone;
587
        // avoid time zone conversion for date-only values
588 170
        if ($type === 'date') {
589 163
            list($timestamp, $hasTimeInfo) = $this->normalizeDatetimeValue($value, true);
590 161
            if (!$hasTimeInfo) {
591 8
                $timeZone = $this->defaultTimeZone;
592 8
            }
593 161
        } else {
594 150
            $timestamp = $this->normalizeDatetimeValue($value);
595
        }
596 168
        if ($timestamp === null) {
597 6
            return $this->nullDisplay;
598
        }
599
600
        // intl does not work with dates >=2038 or <=1901 on 32bit machines, fall back to PHP
601 168
        $year = $timestamp->format('Y');
602 168
        if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
603 80
            if (strncmp($format, 'php:', 4) === 0) {
604 5
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
605 5
            }
606 80
            if (isset($this->_dateFormats[$format])) {
607 3
                if ($type === 'date') {
608 1
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar);
609 3
                } elseif ($type === 'time') {
610 1
                    $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar);
611 1
                } else {
612 1
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar);
613
                }
614 3
            } else {
615 80
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
616
            }
617 80
            if ($formatter === null) {
618
                throw new InvalidConfigException(intl_get_error_message());
619
            }
620
            // make IntlDateFormatter work with DateTimeImmutable
621 80
            if ($timestamp instanceof \DateTimeImmutable) {
0 ignored issues
show
Bug introduced by
The class DateTimeImmutable does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
622 14
                $timestamp = new DateTime($timestamp->format(DateTime::ISO8601), $timestamp->getTimezone());
623 14
            }
624 80
            return $formatter->format($timestamp);
625
        } else {
626 88
            if (strncmp($format, 'php:', 4) === 0) {
627 11
                $format = substr($format, 4);
628 11
            } else {
629 83
                $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
630
            }
631 88
            if ($timeZone != null) {
632 88
                if ($timestamp instanceof \DateTimeImmutable) {
0 ignored issues
show
Bug introduced by
The class DateTimeImmutable does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
633 13
                    $timestamp = $timestamp->setTimezone(new DateTimeZone($timeZone));
634 13
                } else {
635 78
                    $timestamp->setTimezone(new DateTimeZone($timeZone));
636
                }
637 88
            }
638 88
            return $timestamp->format($format);
639
        }
640
    }
641
642
    /**
643
     * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
644
     *
645
     * @param int|string|DateTime $value the datetime value to be normalized. The following
646
     * types of value are supported:
647
     *
648
     * - an integer representing a UNIX timestamp
649
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
650
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
651
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
652
     *
653
     * @param bool $checkTimeInfo whether to also check if the date/time value has some time information attached.
654
     * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
655
     * timestamp and the second a boolean indicating whether the timestamp has time information or it is just a date value.
656
     * This parameter is available since version 2.0.1.
657
     * @return DateTime|array the normalized datetime value.
658
     * Since version 2.0.1 this may also return an array if `$checkTimeInfo` is true.
659
     * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
660
     * the timestamp has time information or it is just a date value.
661
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
662
     */
663 175
    protected function normalizeDatetimeValue($value, $checkTimeInfo = false)
664
    {
665
        // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
666 175
        if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
0 ignored issues
show
Bug introduced by
The class DateTimeInterface does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
667
            // skip any processing
668 50
            return $checkTimeInfo ? [$value, true] : $value;
669
        }
670 135
        if (empty($value)) {
671 10
            $value = 0;
672 10
        }
673
        try {
674 135
            if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
675 30
                $timestamp = new DateTime('@' . (int)$value, new DateTimeZone('UTC'));
676 30
                return $checkTimeInfo ? [$timestamp, true] : $timestamp;
677 108
            } 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)
678 8
                return $checkTimeInfo ? [$timestamp, false] : $timestamp;
679 100
            } 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)
680 18
                return $checkTimeInfo ? [$timestamp, true] : $timestamp;
681
            }
682
            // finally try to create a DateTime object with the value
683 85
            if ($checkTimeInfo) {
684 83
                $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
685 81
                $info = date_parse($value);
686 81
                return [$timestamp, !($info['hour'] === false && $info['minute'] === false && $info['second'] === false)];
687
            } else {
688 83
                return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
689
            }
690 2
        } catch (\Exception $e) {
691 2
            throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
692 2
                . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
693
        }
694
    }
695
696
    /**
697
     * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
698
     * @param int|string|DateTime $value the value to be formatted. The following
699
     * types of value are supported:
700
     *
701
     * - an integer representing a UNIX timestamp
702
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
703
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
704
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
705
     *
706
     * @return string the formatted result.
707
     */
708 144
    public function asTimestamp($value)
709
    {
710 144
        if ($value === null) {
711 2
            return $this->nullDisplay;
712
        }
713 144
        $timestamp = $this->normalizeDatetimeValue($value);
714 144
        return number_format($timestamp->format('U'), 0, '.', '');
715
    }
716
717
    /**
718
     * Formats the value as the time interval between a date and now in human readable form.
719
     *
720
     * This method can be used in three different ways:
721
     *
722
     * 1. Using a timestamp that is relative to `now`.
723
     * 2. Using a timestamp that is relative to the `$referenceTime`.
724
     * 3. Using a `DateInterval` object.
725
     *
726
     * @param int|string|DateTime|DateInterval $value the value to be formatted. The following
727
     * types of value are supported:
728
     *
729
     * - an integer representing a UNIX timestamp
730
     * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
731
     *   The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
732
     * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
733
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
734
     *
735
     * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
736
     * when `$value` is not a `DateInterval` object.
737
     * @return string the formatted result.
738
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
739
     */
740 92
    public function asRelativeTime($value, $referenceTime = null)
741
    {
742 92
        if ($value === null) {
743 2
            return $this->nullDisplay;
744
        }
745
746 92
        if ($value instanceof DateInterval) {
747 2
            $interval = $value;
748 2
        } else {
749 92
            $timestamp = $this->normalizeDatetimeValue($value);
750
751 92
            if ($timestamp === false) {
752
                // $value is not a valid date/time value, so we try
753
                // to create a DateInterval with it
754
                try {
755
                    $interval = new DateInterval($value);
756
                } catch (\Exception $e) {
757
                    // invalid date/time and invalid interval
758
                    return $this->nullDisplay;
759
                }
760
            } else {
761 92
                $timeZone = new DateTimeZone($this->timeZone);
762
763 92
                if ($referenceTime === null) {
764
                    $dateNow = new DateTime('now', $timeZone);
765
                } else {
766 92
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
767 92
                    $dateNow->setTimezone($timeZone);
768
                }
769
770 92
                $dateThen = $timestamp->setTimezone($timeZone);
771
772 92
                $interval = $dateThen->diff($dateNow);
773
            }
774
        }
775
776 92
        if ($interval->invert) {
777 92
            if ($interval->y >= 1) {
778 2
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
779
            }
780 92
            if ($interval->m >= 1) {
781 2
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
782
            }
783 92
            if ($interval->d >= 1) {
784 2
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
785
            }
786 92
            if ($interval->h >= 1) {
787 92
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
788
            }
789 2
            if ($interval->i >= 1) {
790 2
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
791
            }
792 2
            if ($interval->s == 0) {
793
                return Yii::t('yii', 'just now', [], $this->locale);
794
            }
795 2
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
796
        } else {
797 92
            if ($interval->y >= 1) {
798 2
                return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
799
            }
800 92
            if ($interval->m >= 1) {
801 2
                return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
802
            }
803 92
            if ($interval->d >= 1) {
804 2
                return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
805
            }
806 92
            if ($interval->h >= 1) {
807 92
                return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
808
            }
809 2
            if ($interval->i >= 1) {
810 2
                return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
811
            }
812 2
            if ($interval->s == 0) {
813 2
                return Yii::t('yii', 'just now', [], $this->locale);
814
            }
815 2
            return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
816
        }
817
    }
818
819
    /**
820
     * Represents the value as duration in human readable format.
821
     *
822
     * @param DateInterval|string|int $value the value to be formatted. Acceptable formats:
823
     *  - [DateInterval object](http://php.net/manual/ru/class.dateinterval.php)
824
     *  - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
825
     *  - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
826
     *    `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
827
     *    `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
828
     *    `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
829
     *    `P1D2H30M` - simply a date interval
830
     *    `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
831
     *
832
     * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
833
     * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
834
     * @return string the formatted duration.
835
     * @since 2.0.7
836
     */
837 2
    public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
838
    {
839 2
        if ($value === null) {
840 2
            return $this->nullDisplay;
841
        }
842
843 2
        if ($value instanceof DateInterval) {
844 2
            $isNegative = $value->invert;
845 2
            $interval = $value;
846 2
        } elseif (is_numeric($value)) {
847 2
            $isNegative = $value < 0;
848 2
            $zeroDateTime = (new DateTime())->setTimestamp(0);
849 2
            $valueDateTime = (new DateTime())->setTimestamp(abs($value));
850 2
            $interval = $valueDateTime->diff($zeroDateTime);
851 2
        } elseif (strpos($value, 'P-') === 0) {
852 2
            $interval = new DateInterval('P'.substr($value, 2));
853 2
            $isNegative = true;
854 2
        } else {
855 2
            $interval = new DateInterval($value);
856 2
            $isNegative = $interval->invert;
857
        }
858
859 2
        if ($interval->y > 0) {
860 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->locale);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$parts was never initialized. Although not strictly required by PHP, it is generally a good practice to add $parts = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
861 2
        }
862 2
        if ($interval->m > 0) {
863 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->locale);
0 ignored issues
show
Bug introduced by
The variable $parts does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
864 2
        }
865 2
        if ($interval->d > 0) {
866 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale);
867 2
        }
868 2
        if ($interval->h > 0) {
869 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
870 2
        }
871 2
        if ($interval->i > 0) {
872 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
873 2
        }
874 2
        if ($interval->s > 0) {
875 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
876 2
        }
877 2
        if ($interval->s === 0 && empty($parts)) {
878 2
            $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
879 2
            $isNegative = false;
880 2
        }
881
882 2
        return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
883
    }
884
885
886
    // number formats
887
888
889
    /**
890
     * Formats the value as an integer number by removing any decimal digits without rounding.
891
     *
892
     * @param mixed $value the value to be formatted.
893
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
894
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
895
     * @return string the formatted result.
896
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
897
     */
898 7
    public function asInteger($value, $options = [], $textOptions = [])
899
    {
900 7
        if ($value === null) {
901 5
            return $this->nullDisplay;
902
        }
903 7
        $value = $this->normalizeNumericValue($value);
904 5
        if ($this->_intlLoaded) {
905 4
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
906 4
            $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
907 4
            if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
908
                throw new InvalidParamException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
909
            }
910 4
            return $result;
911
        } else {
912 1
            return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
913
        }
914
    }
915
916
    /**
917
     * Formats the value as a decimal number.
918
     *
919
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
920
     * value is rounded automatically to the defined decimal digits.
921
     *
922
     * @param mixed $value the value to be formatted.
923
     * @param int $decimals the number of digits after the decimal point.
924
     * If not given, the number of digits depends in the input value and is determined based on
925
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
926
     * using [[$numberFormatterOptions]].
927
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `2`.
928
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
929
     * specify a value here.
930
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
931
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
932
     * @return string the formatted result.
933
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
934
     * @see decimalSeparator
935
     * @see thousandSeparator
936
     */
937 12
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
938
    {
939 12
        if ($value === null) {
940 2
            return $this->nullDisplay;
941
        }
942 12
        $value = $this->normalizeNumericValue($value);
943
944 12
        if ($this->_intlLoaded) {
945 6
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
946 6
            if (($result = $f->format($value)) === false) {
947
                throw new InvalidParamException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
948
            }
949 6
            return $result;
950
        } else {
951 6
            if ($decimals === null) {
952 4
                $decimals = 2;
953 4
            }
954 6
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
955
        }
956
    }
957
958
959
    /**
960
     * Formats the value as a percent number with "%" sign.
961
     *
962
     * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
963
     * @param int $decimals the number of digits after the decimal point.
964
     * If not given, the number of digits depends in the input value and is determined based on
965
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
966
     * using [[$numberFormatterOptions]].
967
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `0`.
968
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
969
     * specify a value here.
970
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
971
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
972
     * @return string the formatted result.
973
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
974
     */
975 2
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
976
    {
977 2
        if ($value === null) {
978 2
            return $this->nullDisplay;
979
        }
980 2
        $value = $this->normalizeNumericValue($value);
981
982 2
        if ($this->_intlLoaded) {
983 1
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
984 1
            if (($result = $f->format($value)) === false) {
985
                throw new InvalidParamException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
986
            }
987 1
            return $result;
988
        } else {
989 1
            if ($decimals === null) {
990 1
                $decimals = 0;
991 1
            }
992 1
            $value *= 100;
993 1
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
994
        }
995
    }
996
997
    /**
998
     * Formats the value as a scientific number.
999
     *
1000
     * @param mixed $value the value to be formatted.
1001
     * @param int $decimals the number of digits after the decimal point.
1002
     * If not given, the number of digits depends in the input value and is determined based on
1003
     * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
1004
     * using [[$numberFormatterOptions]].
1005
     * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value depends on your PHP configuration.
1006
     * If you want consistent behavior between environments where intl is available and not, you should explicitly
1007
     * specify a value here.
1008
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1009
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1010
     * @return string the formatted result.
1011
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1012
     */
1013 2
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
1014
    {
1015 2
        if ($value === null) {
1016 2
            return $this->nullDisplay;
1017
        }
1018 2
        $value = $this->normalizeNumericValue($value);
1019
1020 2
        if ($this->_intlLoaded) {
1021 1
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
1022 1
            if (($result = $f->format($value)) === false) {
1023
                throw new InvalidParamException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1024
            }
1025 1
            return $result;
1026
        } else {
1027 1
            if ($decimals !== null) {
1028 1
                return sprintf("%.{$decimals}E", $value);
1029
            } else {
1030 1
                return sprintf('%.E', $value);
1031
            }
1032
        }
1033
    }
1034
1035
    /**
1036
     * Formats the value as a currency number.
1037
     *
1038
     * This function does not require the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed
1039
     * to work, but it is highly recommended to install it to get good formatting results.
1040
     *
1041
     * @param mixed $value the value to be formatted.
1042
     * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
1043
     * If null, [[currencyCode]] will be used.
1044
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1045
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1046
     * @return string the formatted result.
1047
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1048
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
1049
     */
1050 4
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
1051
    {
1052 4
        if ($value === null) {
1053 2
            return $this->nullDisplay;
1054
        }
1055 4
        $value = $this->normalizeNumericValue($value);
1056
1057 4
        if ($this->_intlLoaded) {
1058 3
            $currency = $currency ?: $this->currencyCode;
1059
            // currency code must be set before fraction digits
1060
            // http://php.net/manual/en/numberformatter.formatcurrency.php#114376
1061 3
            if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
1062 3
                $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
1063 3
            }
1064 3
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
1065 3
            if ($currency === null) {
1066 2
                $result = $formatter->format($value);
1067 2
            } else {
1068 3
                $result = $formatter->formatCurrency($value, $currency);
1069
            }
1070 3
            if ($result === false) {
1071
                throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
1072
            }
1073 3
            return $result;
1074
        } else {
1075 1
            if ($currency === null) {
1076 1
                if ($this->currencyCode === null) {
1077
                    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.');
1078
                }
1079 1
                $currency = $this->currencyCode;
1080 1
            }
1081 1
            return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
1082
        }
1083
    }
1084
1085
    /**
1086
     * Formats the value as a number spellout.
1087
     *
1088
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1089
     *
1090
     * @param mixed $value the value to be formatted
1091
     * @return string the formatted result.
1092
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1093
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1094
     */
1095 1
    public function asSpellout($value)
1096
    {
1097 1
        if ($value === null) {
1098 1
            return $this->nullDisplay;
1099
        }
1100 1
        $value = $this->normalizeNumericValue($value);
1101 1
        if ($this->_intlLoaded) {
1102 1
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
1103 1
            if (($result = $f->format($value)) === false) {
1104
                throw new InvalidParamException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1105
            }
1106 1
            return $result;
1107
        } else {
1108
            throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
1109
        }
1110
    }
1111
1112
    /**
1113
     * Formats the value as a ordinal value of a number.
1114
     *
1115
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
1116
     *
1117
     * @param mixed $value the value to be formatted
1118
     * @return string the formatted result.
1119
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1120
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
1121
     */
1122 1
    public function asOrdinal($value)
1123
    {
1124 1
        if ($value === null) {
1125 1
            return $this->nullDisplay;
1126
        }
1127 1
        $value = $this->normalizeNumericValue($value);
1128 1
        if ($this->_intlLoaded) {
1129 1
            $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
1130 1
            if (($result = $f->format($value)) === false) {
1131
                throw new InvalidParamException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
1132
            }
1133 1
            return $result;
1134
        } else {
1135
            throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
1136
        }
1137
    }
1138
1139
    /**
1140
     * Formats the value in bytes as a size in human readable form for example `12 KB`.
1141
     *
1142
     * This is the short form of [[asSize]].
1143
     *
1144
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1145
     * are used in the formatting result.
1146
     *
1147
     * @param string|int|float $value value in bytes to be formatted.
1148
     * @param int $decimals the number of digits after the decimal point.
1149
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1150
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1151
     * @return string the formatted result.
1152
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1153
     * @see sizeFormatBase
1154
     * @see asSize
1155
     */
1156 5
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
1157
    {
1158 5
        if ($value === null) {
1159 2
            return $this->nullDisplay;
1160
        }
1161
1162 5
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1163
1164 5
        if ($this->sizeFormatBase == 1024) {
1165
            switch ($position) {
1166 5
                case 0:
1167 5
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1168 3
                case 1:
1169 3
                    return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
1170 3
                case 2:
1171 3
                    return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
1172 2
                case 3:
1173 2
                    return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
1174 2
                case 4:
1175
                    return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
1176 2
                default:
1177 2
                    return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
1178 2
            }
1179
        } else {
1180
            switch ($position) {
1181 2
                case 0:
1182 2
                    return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
1183 2
                case 1:
1184 2
                    return Yii::t('yii', '{nFormatted} KB', $params, $this->locale);
1185 2
                case 2:
1186 2
                    return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
1187 2
                case 3:
1188 2
                    return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
1189 2
                case 4:
1190
                    return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
1191 2
                default:
1192 2
                    return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
1193 2
            }
1194
        }
1195
    }
1196
1197
    /**
1198
     * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
1199
     *
1200
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
1201
     * are used in the formatting result.
1202
     *
1203
     * @param string|int|float $value value in bytes to be formatted.
1204
     * @param int $decimals the number of digits after the decimal point.
1205
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1206
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1207
     * @return string the formatted result.
1208
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1209
     * @see sizeFormatBase
1210
     * @see asShortSize
1211
     */
1212 6
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
1213
    {
1214 6
        if ($value === null) {
1215 2
            return $this->nullDisplay;
1216
        }
1217
1218 6
        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
1219
1220 6
        if ($this->sizeFormatBase == 1024) {
1221
            switch ($position) {
1222 6
                case 0:
1223 6
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1224 4
                case 1:
1225 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
1226 4
                case 2:
1227 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
1228 4
                case 3:
1229 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
1230 4
                case 4:
1231
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
1232 4
                default:
1233 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
1234 4
            }
1235
        } else {
1236
            switch ($position) {
1237 4
                case 0:
1238 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
1239 4
                case 1:
1240 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
1241 4
                case 2:
1242 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
1243 4
                case 3:
1244 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
1245 4
                case 4:
1246
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
1247 4
                default:
1248 4
                    return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
1249 4
            }
1250
        }
1251
    }
1252
1253
1254
    /**
1255
     * Given the value in bytes formats number part of the human readable form.
1256
     *
1257
     * @param string|int|float $value value in bytes to be formatted.
1258
     * @param int $decimals the number of digits after the decimal point
1259
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1260
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1261
     * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
1262
     * @throws InvalidParamException if the input value is not numeric or the formatting failed.
1263
     */
1264 9
    private function formatSizeNumber($value, $decimals, $options, $textOptions)
1265
    {
1266 9
        $value = $this->normalizeNumericValue($value);
1267
1268 9
        $position = 0;
1269
        do {
1270 9
            if (abs($value) < $this->sizeFormatBase) {
1271 9
                break;
1272
            }
1273 7
            $value /= $this->sizeFormatBase;
1274 7
            $position++;
1275 7
        } while ($position < 5);
1276
1277
        // no decimals for bytes
1278 9
        if ($position === 0) {
1279 9
            $decimals = 0;
1280 9
        } elseif ($decimals !== null) {
1281 6
            $value = round($value, $decimals);
1282 6
        }
1283
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
1284 9
        $oldThousandSeparator = $this->thousandSeparator;
1285 9
        $this->thousandSeparator = '';
1286 9
        if ($this->_intlLoaded) {
1287 5
            $options[NumberFormatter::GROUPING_USED] = false;
1288 5
        }
1289
        // format the size value
1290
        $params = [
1291
            // this is the unformatted number used for the plural rule
1292
            // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
1293
            // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
1294 9
            'n' => abs($value),
1295
            // this is the formatted number used for display
1296 9
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
1297 9
        ];
1298 9
        $this->thousandSeparator = $oldThousandSeparator;
1299
1300 9
        return [$params, $position];
1301
    }
1302
1303
    /**
1304
     * Normalizes a numeric input value
1305
     *
1306
     * - everything [empty](http://php.net/manual/en/function.empty.php) will result in `0`
1307
     * - a [numeric](http://php.net/manual/en/function.is-numeric.php) string will be casted to float
1308
     * - everything else will be returned if it is [numeric](http://php.net/manual/en/function.is-numeric.php),
1309
     *   otherwise an exception is thrown.
1310
     *
1311
     * @param mixed $value the input value
1312
     * @return float|int the normalized number value
1313
     * @throws InvalidParamException if the input value is not numeric.
1314
     */
1315 28
    protected function normalizeNumericValue($value)
1316
    {
1317 28
        if (empty($value)) {
1318 16
            return 0;
1319
        }
1320 28
        if (is_string($value) && is_numeric($value)) {
1321 15
            $value = (float) $value;
1322 15
        }
1323 28
        if (!is_numeric($value)) {
1324 2
            throw new InvalidParamException("'$value' is not a numeric value.");
1325
        }
1326 26
        return $value;
1327
    }
1328
1329
    /**
1330
     * Creates a number formatter based on the given type and format.
1331
     *
1332
     * You may override this method to create a number formatter based on patterns.
1333
     *
1334
     * @param int $style the type of the number formatter.
1335
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
1336
     * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
1337
     * @param int $decimals the number of digits after the decimal point.
1338
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
1339
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1340
     * @return NumberFormatter the created formatter instance
1341
     */
1342 17
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1343
    {
1344 17
        $formatter = new NumberFormatter($this->locale, $style);
1345
1346
        // set text attributes
1347 17
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
1348 1
            $formatter->setTextAttribute($name, $attribute);
1349 17
        }
1350 17
        foreach ($textOptions as $name => $attribute) {
1351 3
            $formatter->setTextAttribute($name, $attribute);
1352 17
        }
1353
1354
        // set attributes
1355 17
        foreach ($this->numberFormatterOptions as $name => $value) {
1356 9
            $formatter->setAttribute($name, $value);
1357 17
        }
1358 17
        foreach ($options as $name => $value) {
1359 7
            $formatter->setAttribute($name, $value);
1360 17
        }
1361 17
        if ($decimals !== null) {
1362 6
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
1363 6
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
1364 6
        }
1365
1366
        // set symbols
1367 17
        if ($this->decimalSeparator !== null) {
1368 4
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1369 4
        }
1370 17
        if ($this->thousandSeparator !== null) {
1371 7
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1372 7
            $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1373 7
        }
1374 17
        foreach ($this->numberFormatterSymbols as $name => $symbol) {
1375 2
            $formatter->setSymbol($name, $symbol);
1376 17
        }
1377
1378 17
        return $formatter;
1379
    }
1380
}
1381