Formatter.php 46.2 KB
Newer Older
Qiang Xue committed
1 2 3 4 5 6 7
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

8
namespace yii\i18n;
Erik_r committed
9

Qiang Xue committed
10
use DateTime;
11 12
use IntlDateFormatter;
use NumberFormatter;
Carsten Brandt committed
13
use Yii;
14 15 16
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
17
use yii\helpers\FormatConverter;
Qiang Xue committed
18 19
use yii\helpers\HtmlPurifier;
use yii\helpers\Html;
20

Qiang Xue committed
21
/**
22 23
 * Formatter provides a set of commonly used data formatting methods.
 *
Qiang Xue committed
24 25 26 27
 * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
 * The behavior of some of them may be configured via the properties of Formatter. For example,
 * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
 *
28
 * Formatter is configured as an application component in [[\yii\base\Application]] by default.
29 30
 * You can access that instance via `Yii::$app->formatter`.
 *
31 32 33
 * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
 * the [PHP intl extension](http://php.net/manual/en/book.intl.php) has to be installed.
 * Most of the methods however work also if the PHP intl extension is not installed by providing
34
 * a fallback implementation. Without intl month and day names are in English only.
35 36 37 38 39 40 41
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @author Enrica Ruedin <e.ruedin@guggach.com>
 * @author Carsten Brandt <mail@cebe.cc>
 * @since 2.0
 */
class Formatter extends Component
Qiang Xue committed
42
{
43 44
    /**
     * @var string the text to be displayed when formatting a `null` value.
Carsten Brandt committed
45 46
     * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
     * will be translated according to [[locale]].
47 48 49 50 51
     */
    public $nullDisplay;
    /**
     * @var array the text to be displayed when formatting a boolean value. The first element corresponds
     * to the text displayed for `false`, the second element for `true`.
Carsten Brandt committed
52
     * Defaults to `['No', 'Yes']`, where `Yes` and `No`
53
     * will be translated according to [[locale]].
54 55
     */
    public $booleanFormat;
David Renty committed
56
    /**
57
     * @var string the locale ID that is used to localize the date and number formatting.
58 59
     * For number and date formatting this is only effective when the
     * [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
60 61 62
     * If not set, [[\yii\base\Application::language]] will be used.
     */
    public $locale;
63
    /**
David Renty committed
64 65 66 67
     * @var string the timezone to use for formatting time and date values.
     * 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)
     * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
     * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available timezones.
Carsten Brandt committed
68
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
David Renty committed
69 70 71
     */
    public $timeZone;
    /**
72
     * @var string the default format string to be used to format a [[asDate()|date]].
73
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
74
     *
75
     * 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).
76 77
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
     * PHP [date()](http://php.net/manual/de/function.date.php)-function.
78 79 80 81 82 83 84
     *
     * For example:
     *
     * ```php
     * 'MM/dd/yyyy' // date in ICU format
     * 'php:m/d/Y' // the same date in PHP format
     * ```
David Renty committed
85
     */
86
    public $dateFormat = 'medium';
87
    /**
88
     * @var string the default format string to be used to format a [[asTime()|time]].
89
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
90
     *
91
     * 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).
92 93
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
     * PHP [date()](http://php.net/manual/de/function.date.php)-function.
94 95 96 97 98 99 100
     *
     * For example:
     *
     * ```php
     * 'HH:mm:ss' // time in ICU format
     * 'php:H:i:s' // the same time in PHP format
     * ```
101
     */
102
    public $timeFormat = 'medium';
David Renty committed
103
    /**
104
     * @var string the default format string to be used to format a [[asDateTime()|date and time]].
105
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
106
     *
107
     * 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).
108 109 110
     *
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
     * PHP [date()](http://php.net/manual/de/function.date.php)-function.
111 112 113 114 115 116 117
     *
     * For example:
     *
     * ```php
     * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
     * 'php:m/d/Y H:i:s' // the same date and time in PHP format
     * ```
David Renty committed
118
     */
119
    public $datetimeFormat = 'medium';
David Renty committed
120 121
    /**
     * @var string the character displayed as the decimal point when formatting a number.
122
     * If not set, the decimal separator corresponding to [[locale]] will be used.
123
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'.
David Renty committed
124 125 126
     */
    public $decimalSeparator;
    /**
127
     * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
128
     * If not set, the thousand separator corresponding to [[locale]] will be used.
129
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is ','.
David Renty committed
130 131
     */
    public $thousandSeparator;
132 133 134 135 136 137 138 139 140
    /**
     * @var array a list of name value pairs that are passed to the
     * intl [Numberformatter::setAttribute()](http://php.net/manual/en/numberformatter.setattribute.php) method of all
     * the number formatter objects created by [[createNumberFormatter()]].
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
     *
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
     * for the possible options.
     *
Carsten Brandt committed
141
     * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
142 143 144
     *
     * ```php
     * [
Carsten Brandt committed
145 146
     *     NumberFormatter::MIN_FRACTION_DIGITS => 0,
     *     NumberFormatter::MAX_FRACTION_DIGITS => 2,
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
     * ]
     * ```
     */
    public $numberFormatterOptions = [];
    /**
     * @var array a list of name value pairs that are passed to the
     * intl [Numberformatter::setTextAttribute()](http://php.net/manual/en/numberformatter.settextattribute.php) method of all
     * the number formatter objects created by [[createNumberFormatter()]].
     * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
     *
     * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
     * for the possible options.
     *
     * For example to change the minus sign for negative numbers you can configure this property like the following:
     *
     * ```php
     * [
     *     NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
     * ]
     * ```
     */
    public $numberFormatterTextOptions = [];
169
    /**
170
     * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
Carsten Brandt committed
171
     * If not set, the currency code corresponding to [[locale]] will be used.
Carsten Brandt committed
172 173
     * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
     * is not possible to determine the default currency.
174 175
     */
    public $currencyCode;
David Renty committed
176
    /**
Carsten Brandt committed
177 178
     * @var array the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
     * Defaults to 1024.
David Renty committed
179
     */
Carsten Brandt committed
180
    public $sizeFormatBase = 1024;
David Renty committed
181

182
    /**
183
     * @var boolean whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded.
184 185 186
     */
    private $_intlLoaded = false;

187

188
    /**
189
     * @inheritdoc
David Renty committed
190 191 192 193 194 195
     */
    public function init()
    {
        if ($this->timeZone === null) {
            $this->timeZone = Yii::$app->timeZone;
        }
196 197 198
        if ($this->locale === null) {
            $this->locale = Yii::$app->language;
        }
199
        if ($this->booleanFormat === null) {
200
            $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
David Renty committed
201 202
        }
        if ($this->nullDisplay === null) {
203
            $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
David Renty committed
204
        }
205
        $this->_intlLoaded = extension_loaded('intl');
206 207 208 209
        if (!$this->_intlLoaded) {
            $this->decimalSeparator = '.';
            $this->thousandSeparator = ',';
        }
David Renty committed
210 211
    }

212
    /**
David Renty committed
213 214 215 216
     * Formats the value based on the given format type.
     * This method will call one of the "as" methods available in this class to do the formatting.
     * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
     * then [[asHtml()]] will be used. Format names are case insensitive.
217
     * @param mixed $value the value to be formatted.
218 219 220
     * @param string|array $format the format of the value, e.g., "html", "text". To specify additional
     * parameters of the formatting method, you may use an array. The first element of the array
     * specifies the format name, while the rest of the elements will be used as the parameters to the formatting
221 222 223
     * method. For example, a format of `['date', 'Y-m-d']` will cause the invocation of `asDate($value, 'Y-m-d')`.
     * @return string the formatting result.
     * @throws InvalidParamException if the format type is not supported by this class.
David Renty committed
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
     */
    public function format($value, $format)
    {
        if (is_array($format)) {
            if (!isset($format[0])) {
                throw new InvalidParamException('The $format array must contain at least one element.');
            }
            $f = $format[0];
            $format[0] = $value;
            $params = $format;
            $format = $f;
        } else {
            $params = [$value];
        }
        $method = 'as' . $format;
        if ($this->hasMethod($method)) {
            return call_user_func_array([$this, $method], $params);
        } else {
242
            throw new InvalidParamException("Unknown format type: $format");
243 244 245 246
        }
    }


247
    // simple formats
248 249


David Renty committed
250 251 252
    /**
     * Formats the value as is without any formatting.
     * This method simply returns back the parameter without any format.
253 254
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
255 256 257 258 259 260 261 262
     */
    public function asRaw($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        return $value;
    }
263

David Renty committed
264 265
    /**
     * Formats the value as an HTML-encoded plain text.
266 267
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
268 269 270 271 272 273 274 275
     */
    public function asText($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        return Html::encode($value);
    }
276

David Renty committed
277 278
    /**
     * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
279 280
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
281 282 283 284 285 286 287 288 289 290 291 292 293
     */
    public function asNtext($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        return nl2br(Html::encode($value));
    }

    /**
     * Formats the value as HTML-encoded text paragraphs.
     * Each text paragraph is enclosed within a `<p>` tag.
     * One or multiple consecutive empty lines divide two paragraphs.
294 295
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
296 297 298 299 300 301
     */
    public function asParagraphs($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
302
        return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/', "</p>\n<p>", Html::encode($value)) . '</p>');
David Renty committed
303 304 305 306 307 308
    }

    /**
     * Formats the value as HTML text.
     * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
     * Use [[asRaw()]] if you do not want any purification of the value.
309
     * @param mixed $value the value to be formatted.
310
     * @param array|null $config the configuration for the HTMLPurifier class.
311
     * @return string the formatted result.
David Renty committed
312 313 314 315 316 317 318 319 320 321 322
     */
    public function asHtml($value, $config = null)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        return HtmlPurifier::process($value, $config);
    }

    /**
     * Formats the value as a mailto link.
323 324
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
325 326 327 328 329 330 331 332 333 334 335
     */
    public function asEmail($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        return Html::mailto(Html::encode($value), $value);
    }

    /**
     * Formats the value as an image tag.
336 337
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
338 339 340 341 342 343 344 345 346 347 348
     */
    public function asImage($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        return Html::img($value);
    }

    /**
     * Formats the value as a hyperlink.
349 350
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
351 352 353 354 355 356 357
     */
    public function asUrl($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        $url = $value;
Carsten Brandt committed
358
        if (strpos($url, '://') === false) {
David Renty committed
359 360
            $url = 'http://' . $url;
        }
361

David Renty committed
362 363 364 365 366
        return Html::a(Html::encode($value), $url);
    }

    /**
     * Formats the value as a boolean.
367 368
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
369 370 371 372 373 374 375
     * @see booleanFormat
     */
    public function asBoolean($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
376

David Renty committed
377 378
        return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
    }
379 380 381 382 383


    // date and time formats


David Renty committed
384 385 386
    /**
     * Formats the value as a date.
     * @param integer|string|DateTime $value the value to be formatted. The following
387
     * types of value are supported:
David Renty committed
388 389
     *
     * - an integer representing a UNIX timestamp
390
     * - a string that can be parsed into a UNIX timestamp via `strtotime()`
David Renty committed
391 392
     * - a PHP DateTime object
     *
393 394 395 396 397 398 399 400 401
     * @param string $format the format used to convert the value into a date string.
     * If null, [[dateFormat]] will be used.
     *
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
     *
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
     * PHP [date()](http://php.net/manual/de/function.date.php)-function.
     *
402 403
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
     * @throws InvalidConfigException if the date format is invalid.
404
     * @return string the formatted result.
David Renty committed
405 406
     * @see dateFormat
     */
407
    public function asDate($value, $format = null)
David Renty committed
408
    {
409 410
        if ($format === null) {
            $format = $this->dateFormat;
411
        }
412
        return $this->formatDateTimeValue($value, $format, 'date');
413
    }
414

David Renty committed
415 416 417
    /**
     * Formats the value as a time.
     * @param integer|string|DateTime $value the value to be formatted. The following
418
     * types of value are supported:
David Renty committed
419 420
     *
     * - an integer representing a UNIX timestamp
421
     * - a string that can be parsed into a UNIX timestamp via `strtotime()`
David Renty committed
422 423
     * - a PHP DateTime object
     *
424
     * @param string $format the format used to convert the value into a date string.
425 426 427 428 429 430 431 432
     * If null, [[timeFormat]] will be used.
     *
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
     *
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
     * PHP [date()](http://php.net/manual/de/function.date.php)-function.
     *
433 434
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
     * @throws InvalidConfigException if the date format is invalid.
435
     * @return string the formatted result.
David Renty committed
436 437
     * @see timeFormat
     */
438
    public function asTime($value, $format = null)
David Renty committed
439
    {
440 441
        if ($format === null) {
            $format = $this->timeFormat;
David Renty committed
442
        }
443 444 445 446 447 448 449 450 451
        return $this->formatDateTimeValue($value, $format, 'time');
    }

    /**
     * Formats the value as a datetime.
     * @param integer|string|DateTime $value the value to be formatted. The following
     * types of value are supported:
     *
     * - an integer representing a UNIX timestamp
452
     * - a string that can be parsed into a UNIX timestamp via `strtotime()`
453 454 455 456 457 458 459 460 461 462 463
     * - a PHP DateTime object
     *
     * @param string $format the format used to convert the value into a date string.
     * If null, [[dateFormat]] will be used.
     *
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
     * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
     *
     * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
     * PHP [date()](http://php.net/manual/de/function.date.php)-function.
     *
464 465
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
     * @throws InvalidConfigException if the date format is invalid.
466
     * @return string the formatted result.
467 468 469 470 471 472
     * @see datetimeFormat
     */
    public function asDatetime($value, $format = null)
    {
        if ($format === null) {
            $format = $this->datetimeFormat;
473
        }
474 475 476
        return $this->formatDateTimeValue($value, $format, 'datetime');
    }

477 478 479
    /**
     * @var array map of short format names to IntlDateFormatter constant values.
     */
480 481 482 483 484 485 486 487 488 489
    private $_dateFormats = [
        'short'  => 3, // IntlDateFormatter::SHORT,
        'medium' => 2, // IntlDateFormatter::MEDIUM,
        'long'   => 1, // IntlDateFormatter::LONG,
        'full'   => 0, // IntlDateFormatter::FULL,
    ];

    /**
     * @param integer $value normalized datetime value
     * @param string $format the format used to convert the value into a date string.
490 491
     * @param string $type 'date', 'time', or 'datetime'.
     * @throws InvalidConfigException if the date format is invalid.
492
     * @return string the formatted result.
493 494 495
     */
    private function formatDateTimeValue($value, $format, $type)
    {
496 497
        $timestamp = $this->normalizeDatetimeValue($value);
        if ($timestamp === null) {
498
            return $this->nullDisplay;
499 500
        }

501
        if ($this->_intlLoaded) {
502
            if (strncmp($format, 'php:', 4) === 0) {
503 504
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
            }
505 506 507 508 509 510 511
            if (isset($this->_dateFormats[$format])) {
                if ($type === 'date') {
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $this->timeZone);
                } elseif ($type === 'time') {
                    $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $this->timeZone);
                } else {
                    $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $this->timeZone);
512 513
                }
            } else {
514 515
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone, null, $format);
            }
516 517 518
            if ($formatter === null) {
                throw new InvalidConfigException(intl_get_error_message());
            }
519
            return $formatter->format($timestamp);
520
        } else {
521
            if (strncmp($format, 'php:', 4) === 0) {
522
                $format = substr($format, 4);
523
            } else {
524
                $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
525
            }
526 527 528 529
            if ($this->timeZone != null) {
                $timestamp->setTimezone(new \DateTimeZone($this->timeZone));
            }
            return $timestamp->format($format);
530 531
        }
    }
532

David Renty committed
533
    /**
534
     * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
Kartik Visweswaran committed
535
     *
536
     * @param mixed $value the datetime value to be normalized.
537 538
     * @return DateTime the normalized datetime value
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
David Renty committed
539
     */
540
    protected function normalizeDatetimeValue($value)
David Renty committed
541
    {
542 543
        if ($value === null || $value instanceof DateTime) {
            // skip any processing
Kartik Visweswaran committed
544
            return $value;
545 546 547 548 549
        }
        if (empty($value)) {
            $value = 0;
        }
        try {
550
            if (is_numeric($value)) { // process as unix timestamp
551 552 553 554
                if (($timestamp = DateTime::createFromFormat('U', $value)) === false) {
                    throw new InvalidParamException("Failed to parse '$value' as a UNIX timestamp.");
                }
                return $timestamp;
555 556 557 558
            } elseif (($timestamp = DateTime::createFromFormat('Y-m-d', $value)) !== false) { // try Y-m-d format
                return $timestamp;
            } elseif (($timestamp = DateTime::createFromFormat('Y-m-d H:i:s', $value)) !== false) { // try Y-m-d H:i:s format
                return $timestamp;
559
            }
560
            // finally try to create a DateTime object with the value
561 562 563 564 565
            $timestamp = new DateTime($value);
            return $timestamp;
        } catch(\Exception $e) {
            throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
                . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
566 567 568
        }
    }

569
    /**
570 571 572 573 574 575 576 577 578 579
     * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
     * @param integer|string|DateTime|\DateInterval $value the value to be formatted. The following
     * types of value are supported:
     *
     * - an integer representing a UNIX timestamp
     * - a string that can be parsed into a UNIX timestamp via `strtotime()` or that can be passed to a DateInterval constructor.
     * - a PHP DateTime object
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
     *
     * @return string the formatted result.
580 581 582 583 584 585
     */
    public function asTimestamp($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
586 587
        $timestamp = $this->normalizeDatetimeValue($value);
        return number_format($timestamp->format('U'), 0, '.', '');
588 589 590 591 592 593 594 595 596 597 598 599 600
    }

    /**
     * Formats the value as the time interval between a date and now in human readable form.
     *
     * @param integer|string|DateTime|\DateInterval $value the value to be formatted. The following
     * types of value are supported:
     *
     * - an integer representing a UNIX timestamp
     * - a string that can be parsed into a UNIX timestamp via `strtotime()` or that can be passed to a DateInterval constructor.
     * - a PHP DateTime object
     * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
     *
601 602
     * @param integer|string|DateTime|\DateInterval $referenceTime if specified the value is used instead of `now`.
     * @return string the formatted result.
603
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630
     */
    public function asRelativeTime($value, $referenceTime = null)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }

        if ($value instanceof \DateInterval) {
            $interval = $value;
        } else {
            $timestamp = $this->normalizeDatetimeValue($value);

            if ($timestamp === false) {
                // $value is not a valid date/time value, so we try
                // to create a DateInterval with it
                try {
                    $interval = new \DateInterval($value);
                } catch (\Exception $e) {
                    // invalid date/time and invalid interval
                    return $this->nullDisplay;
                }
            } else {
                $timezone = new \DateTimeZone($this->timeZone);

                if ($referenceTime === null) {
                    $dateNow = new DateTime('now', $timezone);
                } else {
631 632
                    $dateNow = $this->normalizeDatetimeValue($referenceTime);
                    $dateNow->setTimezone($timezone);
633 634
                }

635
                $dateThen = $timestamp->setTimezone($timezone);
636 637 638 639 640 641 642

                $interval = $dateThen->diff($dateNow);
            }
        }

        if ($interval->invert) {
            if ($interval->y >= 1) {
643
                return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
644 645
            }
            if ($interval->m >= 1) {
646
                return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
647 648
            }
            if ($interval->d >= 1) {
649
                return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
650 651
            }
            if ($interval->h >= 1) {
652
                return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
653 654
            }
            if ($interval->i >= 1) {
655
                return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
656
            }
657 658 659
            if ($interval->s == 0) {
                return Yii::t('yii', 'just now', [], $this->locale);
            }
660
            return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
661 662
        } else {
            if ($interval->y >= 1) {
663
                return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
664 665
            }
            if ($interval->m >= 1) {
666
                return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
667 668
            }
            if ($interval->d >= 1) {
669
                return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
670 671
            }
            if ($interval->h >= 1) {
672
                return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
673 674
            }
            if ($interval->i >= 1) {
675
                return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
676
            }
677 678 679
            if ($interval->s == 0) {
                return Yii::t('yii', 'just now', [], $this->locale);
            }
680
            return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
681 682 683
        }
    }

684 685 686 687

    // number formats


David Renty committed
688
    /**
689 690 691 692 693 694
     * Formats the value as an integer number by removing any decimal digits without rounding.
     *
     * @param mixed $value the value to be formatted.
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
     * @return string the formatted result.
695
     * @throws InvalidParamException if the input value is not numeric.
David Renty committed
696
     */
697
    public function asInteger($value, $options = [], $textOptions = [])
698
    {
David Renty committed
699 700 701
        if ($value === null) {
            return $this->nullDisplay;
        }
702 703
        $value = $this->normalizeNumericValue($value);
        if ($this->_intlLoaded) {
704
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
705
            return $f->format($value, NumberFormatter::TYPE_INT64);
David Renty committed
706
        } else {
707
            return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
David Renty committed
708 709
        }
    }
710 711

    /**
712 713
     * Formats the value as a decimal number.
     *
714
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
715 716
     * value is rounded automatically to the defined decimal digits.
     *
717
     * @param mixed $value the value to be formatted.
718 719
     * @param integer $decimals the number of digits after the decimal point. If not given the number of digits is determined from the
     * [[locale]] and if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available defaults to `2`.
720 721 722
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
     * @return string the formatted result.
723
     * @throws InvalidParamException if the input value is not numeric.
David Renty committed
724
     * @see decimalSeparator
725
     * @see thousandSeparator
David Renty committed
726
     */
727
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
David Renty committed
728 729 730 731
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
732
        $value = $this->normalizeNumericValue($value);
733

734
        if ($this->_intlLoaded) {
735
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
736
            return $f->format($value);
David Renty committed
737
        } else {
738 739 740
            if ($decimals === null){
                $decimals = 2;
            }
741
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
David Renty committed
742 743 744
        }
    }

745

David Renty committed
746
    /**
747
     * Formats the value as a percent number with "%" sign.
748 749 750 751 752
     *
     * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
     * @param integer $decimals the number of digits after the decimal point.
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
753
     * @return string the formatted result.
754
     * @throws InvalidParamException if the input value is not numeric.
755
     */
756
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
757 758 759 760
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
761
        $value = $this->normalizeNumericValue($value);
762

763
        if ($this->_intlLoaded) {
764
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
765 766
            return $f->format($value);
        } else {
767 768 769
            if ($decimals === null){
                $decimals = 0;
            }
770
            $value = $value * 100;
771
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
772 773
        }
    }
774 775

    /**
776
     * Formats the value as a scientific number.
777
     *
778 779 780 781
     * @param mixed $value the value to be formatted.
     * @param integer $decimals the number of digits after the decimal point.
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
782
     * @return string the formatted result.
783
     * @throws InvalidParamException if the input value is not numeric.
784
     */
785
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
786
    {
David Renty committed
787 788 789
        if ($value === null) {
            return $this->nullDisplay;
        }
790
        $value = $this->normalizeNumericValue($value);
791

792
        if ($this->_intlLoaded){
793
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
794 795
            return $f->format($value);
        } else {
796
            if ($decimals !== null) {
797
                return sprintf("%.{$decimals}E", $value);
798 799 800
            } else {
                return sprintf("%.E", $value);
            }
801
        }
David Renty committed
802
    }
803 804

    /**
805
     * Formats the value as a currency number.
806 807 808 809
     *
     * This function does not requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed
     * to work but it is highly recommended to install it to get good formatting results.
     *
810
     * @param mixed $value the value to be formatted.
811
     * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
Carsten Brandt committed
812
     * If null, [[currencyCode]] will be used.
813 814
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
815
     * @return string the formatted result.
816
     * @throws InvalidParamException if the input value is not numeric.
817
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
818
     */
819
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
820 821 822 823
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
824
        $value = $this->normalizeNumericValue($value);
825 826

        if ($this->_intlLoaded) {
827
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
Carsten Brandt committed
828 829 830 831 832 833 834
            if ($currency === null) {
                if ($this->currencyCode === null) {
                    $currency = $formatter->getSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL);
                } else {
                    $currency = $this->currencyCode;
                }
            }
835
            return $formatter->formatCurrency($value, $currency);
836
        } else {
Carsten Brandt committed
837 838 839 840 841 842
            if ($currency === null) {
                if ($this->currencyCode === null) {
                    throw new InvalidConfigException('The default currency code for the formatter is not defined.');
                }
                $currency = $this->currencyCode;
            }
843
            return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
844 845
        }
    }
846

847 848
    /**
     * Formats the value as a number spellout.
849 850 851
     *
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
     *
852 853 854
     * @param mixed $value the value to be formatted
     * @return string the formatted result.
     * @throws InvalidParamException if the input value is not numeric.
855
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
856 857 858 859 860 861 862 863 864 865 866
     */
    public function asSpellout($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        $value = $this->normalizeNumericValue($value);
        if ($this->_intlLoaded){
            $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
            return $f->format($value);
        } else {
867
            throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
868 869
        }
    }
870

871 872
    /**
     * Formats the value as a ordinal value of a number.
873 874 875
     *
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
     *
876 877 878
     * @param mixed $value the value to be formatted
     * @return string the formatted result.
     * @throws InvalidParamException if the input value is not numeric.
879
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
880 881 882 883 884 885 886 887 888 889 890
     */
    public function asOrdinal($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        $value = $this->normalizeNumericValue($value);
        if ($this->_intlLoaded){
            $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
            return $f->format($value);
        } else {
891
            throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
892 893
        }
    }
894

David Renty committed
895
    /**
Carsten Brandt committed
896 897 898 899 900 901 902 903 904 905 906
     * Formats the value in bytes as a size in human readable form for example `12 KB`.
     *
     * This is the short form of [[asSize]].
     *
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
     * are used in the formatting result.
     *
     * @param integer $value value in bytes to be formatted.
     * @param integer $decimals the number of digits after the decimal point.
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
907
     * @return string the formatted result.
908
     * @throws InvalidParamException if the input value is not numeric.
David Renty committed
909
     * @see sizeFormat
Carsten Brandt committed
910
     * @see asSize
David Renty committed
911
     */
Carsten Brandt committed
912
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
David Renty committed
913
    {
914 915 916
        if ($value === null) {
            return $this->nullDisplay;
        }
Carsten Brandt committed
917 918 919 920 921

        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);

        if ($this->sizeFormatBase == 1024) {
            switch ($position) {
922 923 924 925 926 927
                case 0:  return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
                case 1:  return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
                case 2:  return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
                case 3:  return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
                case 4:  return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
                default: return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
David Renty committed
928
            }
Carsten Brandt committed
929 930
        } else {
            switch ($position) {
931 932 933 934 935 936
                case 0:  return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
                case 1:  return Yii::t('yii', '{nFormatted} KB', $params, $this->locale);
                case 2:  return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
                case 3:  return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
                case 4:  return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
                default: return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
Carsten Brandt committed
937 938 939
            }
        }
    }
David Renty committed
940

Carsten Brandt committed
941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962
    /**
     * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
     *
     * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
     * are used in the formatting result.
     *
     * @param integer $value value in bytes to be formatted.
     * @param integer $decimals the number of digits after the decimal point.
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
     * @return string the formatted result.
     * @throws InvalidParamException if the input value is not numeric.
     * @see sizeFormat
     * @see asShortSize
     */
    public function asSize($value, $decimals = null, $options = [], $textOptions = [])
    {
        if ($value === null) {
            return $this->nullDisplay;
        }

        list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
963

Carsten Brandt committed
964 965
        if ($this->sizeFormatBase == 1024) {
            switch ($position) {
966 967 968 969 970 971
                case 0:  return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
                case 1:  return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
                case 2:  return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
                case 3:  return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
                case 4:  return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
                default: return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
Carsten Brandt committed
972 973
            }
        } else {
974
            switch ($position) {
975 976 977 978 979 980
                case 0:  return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
                case 1:  return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
                case 2:  return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
                case 3:  return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
                case 4:  return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
                default: return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
981 982
            }
        }
Carsten Brandt committed
983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001
    }

    private function formatSizeNumber($value, $decimals, $options, $textOptions)
    {
        if (is_string($value) && is_numeric($value)) {
            $value = (int) $value;
        }
        if (!is_numeric($value)) {
            throw new InvalidParamException("'$value' is not a numeric value.");
        }

        $position = 0;
        do {
            if ($value < $this->sizeFormatBase) {
                break;
            }
            $value = $value / $this->sizeFormatBase;
            $position++;
        } while ($position < 5);
1002

Carsten Brandt committed
1003 1004 1005 1006 1007
        // no decimals for bytes
        if ($position === 0) {
            $decimals = 0;
        } elseif ($decimals !== null) {
            $value = round($value, $decimals);
David Renty committed
1008
        }
Carsten Brandt committed
1009 1010 1011
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
        $oldThousandSeparator = $this->thousandSeparator;
        $this->thousandSeparator = '';
1012 1013 1014
        if ($this->_intlLoaded) {
            $options[NumberFormatter::GROUPING_USED] = false;
        }
Carsten Brandt committed
1015
        // format the size value
1016 1017 1018 1019 1020 1021
        $params = [
            // this is the unformatted number used for the plural rule
            'n' => $value,
            // this is the formatted number used for display
            'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
        ];
Carsten Brandt committed
1022 1023 1024
        $this->thousandSeparator = $oldThousandSeparator;

        return [$params, $position];
David Renty committed
1025 1026
    }

1027 1028 1029 1030 1031
    /**
     * @param $value
     * @return float
     * @throws InvalidParamException
     */
1032 1033
    protected function normalizeNumericValue($value)
    {
1034 1035 1036
        if (empty($value)) {
            return 0;
        }
1037 1038 1039 1040 1041 1042 1043 1044 1045
        if (is_string($value) && is_numeric($value)) {
            $value = (float) $value;
        }
        if (!is_numeric($value)) {
            throw new InvalidParamException("'$value' is not a numeric value.");
        }
        return $value;
    }

1046 1047
    /**
     * Creates a number formatter based on the given type and format.
1048
     *
Carsten Brandt committed
1049
     * You may overide this method to create a number formatter based on patterns.
1050
     *
Carsten Brandt committed
1051
     * @param integer $style the type of the number formatter.
1052 1053
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
     *          ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
Carsten Brandt committed
1054 1055 1056
     * @param integer $decimals the number of digits after the decimal point.
     * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
     * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
1057 1058
     * @return NumberFormatter the created formatter instance
     */
1059
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1060
    {
1061 1062 1063
        $formatter = new NumberFormatter($this->locale, $style);

        if ($this->decimalSeparator !== null) {
1064
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1065 1066
        }
        if ($this->thousandSeparator !== null) {
1067
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1068
        }
1069

1070 1071 1072 1073
        if ($decimals !== null) {
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
        }
1074

Carsten Brandt committed
1075
        foreach ($this->numberFormatterOptions as $name => $value) {
1076 1077
            $formatter->setAttribute($name, $value);
        }
Carsten Brandt committed
1078 1079 1080 1081 1082 1083 1084
        foreach ($options as $name => $value) {
            $formatter->setAttribute($name, $value);
        }
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
            $formatter->setTextAttribute($name, $attribute);
        }
        foreach ($textOptions as $name => $attribute) {
1085 1086
            $formatter->setTextAttribute($name, $attribute);
        }
1087 1088
        return $formatter;
    }
Qiang Xue committed
1089
}