Formatter.php 45.4 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;
Carsten Brandt committed
11
use DateTimeInterface;
12 13
use IntlDateFormatter;
use NumberFormatter;
Carsten Brandt committed
14
use Yii;
15 16 17
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
18
use yii\helpers\FormatConverter;
Qiang Xue committed
19 20
use yii\helpers\HtmlPurifier;
use yii\helpers\Html;
21

Qiang Xue committed
22
/**
23 24
 * Formatter provides a set of commonly used data formatting methods. 
 * 
Qiang Xue committed
25 26 27 28
 * 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.
 *
29
 * Formatter is configured as an application component in [[\yii\base\Application]] by default.
30 31
 * You can access that instance via `Yii::$app->formatter`.
 *
32 33 34
 * 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
35
 * a fallback implementation. Without intl month and day names are in English only.
36 37 38 39 40 41 42
 *
 * @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
43
{
44 45
    /**
     * @var string the text to be displayed when formatting a `null` value.
Carsten Brandt committed
46 47
     * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
     * will be translated according to [[locale]].
48 49 50 51 52
     */
    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
53
     * Defaults to `['No', 'Yes']`, where `Yes` and `No`
54
     * will be translated according to [[locale]].
55 56
     */
    public $booleanFormat;
David Renty committed
57
    /**
58
     * @var string the locale ID that is used to localize the date and number formatting.
59 60
     * For number and date formatting this is only effective when the
     * [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
61 62 63
     * If not set, [[\yii\base\Application::language]] will be used.
     */
    public $locale;
64
    /**
David Renty committed
65 66 67 68
     * @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
69
     * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
David Renty committed
70 71 72
     */
    public $timeZone;
    /**
73
     * @var string the default format string to be used to format a [[asDate()|date]].
74
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
75
     *
76
     * 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).
77 78
     * 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.
79 80 81 82 83 84 85
     *
     * For example:
     *
     * ```php
     * 'MM/dd/yyyy' // date in ICU format
     * 'php:m/d/Y' // the same date in PHP format
     * ```
David Renty committed
86
     */
87
    public $dateFormat = 'medium';
88
    /**
89
     * @var string the default format string to be used to format a [[asTime()|time]].
90
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
91
     *
92
     * 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).
93 94
     * 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.
95 96 97 98 99 100 101
     *
     * For example:
     *
     * ```php
     * 'HH:mm:ss' // time in ICU format
     * 'php:H:i:s' // the same time in PHP format
     * ```
102
     */
103
    public $timeFormat = 'medium';
David Renty committed
104
    /**
105
     * @var string the default format string to be used to format a [[asDateTime()|date and time]].
106
     * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
107
     *
108
     * 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).
109 110 111
     *
     * 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.
112 113 114 115 116 117 118
     *
     * 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
119
     */
120
    public $datetimeFormat = 'medium';
David Renty committed
121 122
    /**
     * @var string the character displayed as the decimal point when formatting a number.
123
     * If not set, the decimal separator corresponding to [[locale]] will be used.
124
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'.
David Renty committed
125 126 127
     */
    public $decimalSeparator;
    /**
128
     * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
129
     * If not set, the thousand separator corresponding to [[locale]] will be used.
130
     * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is ','.
David Renty committed
131 132
     */
    public $thousandSeparator;
133 134 135 136 137 138 139 140 141
    /**
     * @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
142
     * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
143 144 145
     *
     * ```php
     * [
Carsten Brandt committed
146 147
     *     NumberFormatter::MIN_FRACTION_DIGITS => 0,
     *     NumberFormatter::MAX_FRACTION_DIGITS => 2,
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
     * ]
     * ```
     */
    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 = [];
170
    /**
171
     * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
Carsten Brandt committed
172
     * If not set, the currency code corresponding to [[locale]] will be used.
Carsten Brandt committed
173 174
     * 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.
175 176
     */
    public $currencyCode;
David Renty committed
177
    /**
Carsten Brandt committed
178 179
     * @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
180
     */
Carsten Brandt committed
181
    public $sizeFormatBase = 1024;
David Renty committed
182

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

188

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

213
    /**
David Renty committed
214 215 216 217
     * 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.
218
     * @param mixed $value the value to be formatted.
219 220 221
     * @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
222 223 224
     * 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
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
     */
    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 {
243
            throw new InvalidParamException("Unknown format type: $format");
244 245 246 247
        }
    }


248
    // simple formats
249 250


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

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

David Renty committed
278 279
    /**
     * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
280 281
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
282 283 284 285 286 287 288 289 290 291 292 293 294
     */
    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.
295 296
     * @param mixed $value the value to be formatted.
     * @return string the formatted result.
David Renty committed
297 298 299 300 301 302
     */
    public function asParagraphs($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
303
        return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/', "</p>\n<p>", Html::encode($value)) . '</p>');
David Renty committed
304 305 306 307 308 309
    }

    /**
     * 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.
310
     * @param mixed $value the value to be formatted.
311
     * @param array|null $config the configuration for the HTMLPurifier class.
312
     * @return string the formatted result.
David Renty committed
313 314 315 316 317 318 319 320 321 322 323
     */
    public function asHtml($value, $config = null)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        return HtmlPurifier::process($value, $config);
    }

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

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

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

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

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

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


    // date and time formats


David Renty committed
385 386 387
    /**
     * Formats the value as a date.
     * @param integer|string|DateTime $value the value to be formatted. The following
388
     * types of value are supported:
David Renty committed
389 390
     *
     * - an integer representing a UNIX timestamp
391
     * - a string that can be parsed into a UNIX timestamp via `strtotime()`
David Renty committed
392 393
     * - a PHP DateTime object
     *
394 395 396 397 398 399 400 401 402
     * @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.
     *
403 404
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
     * @throws InvalidConfigException if the date format is invalid.
405
     * @return string the formatted result.
David Renty committed
406 407
     * @see dateFormat
     */
408
    public function asDate($value, $format = null)
David Renty committed
409
    {
410 411
        if ($format === null) {
            $format = $this->dateFormat;
412
        }
413
        return $this->formatDateTimeValue($value, $format, 'date');
414
    }
415

David Renty committed
416 417 418
    /**
     * Formats the value as a time.
     * @param integer|string|DateTime $value the value to be formatted. The following
419
     * types of value are supported:
David Renty committed
420 421
     *
     * - an integer representing a UNIX timestamp
422
     * - a string that can be parsed into a UNIX timestamp via `strtotime()`
David Renty committed
423 424
     * - a PHP DateTime object
     *
425
     * @param string $format the format used to convert the value into a date string.
426 427 428 429 430 431 432 433
     * 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.
     *
434 435
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
     * @throws InvalidConfigException if the date format is invalid.
436
     * @return string the formatted result.
David Renty committed
437 438
     * @see timeFormat
     */
439
    public function asTime($value, $format = null)
David Renty committed
440
    {
441 442
        if ($format === null) {
            $format = $this->timeFormat;
David Renty committed
443
        }
444 445 446 447 448 449 450 451 452
        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
453
     * - a string that can be parsed into a UNIX timestamp via `strtotime()`
454 455 456 457 458 459 460 461 462 463 464
     * - 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.
     *
465 466
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
     * @throws InvalidConfigException if the date format is invalid.
467
     * @return string the formatted result.
468 469 470 471 472 473
     * @see datetimeFormat
     */
    public function asDatetime($value, $format = null)
    {
        if ($format === null) {
            $format = $this->datetimeFormat;
474
        }
475 476 477
        return $this->formatDateTimeValue($value, $format, 'datetime');
    }

478 479 480
    /**
     * @var array map of short format names to IntlDateFormatter constant values.
     */
481 482 483 484 485 486 487 488 489 490
    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.
491 492
     * @param string $type 'date', 'time', or 'datetime'.
     * @throws InvalidConfigException if the date format is invalid.
493
     * @return string the formatted result.
494 495 496 497 498 499
     */
    private function formatDateTimeValue($value, $format, $type)
    {
        $value = $this->normalizeDatetimeValue($value);
        if ($value === null) {
            return $this->nullDisplay;
500 501
        }

502
        if ($this->_intlLoaded) {
503
            if (strncmp($format, 'php:', 4) === 0) {
504 505
                $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
            }
506 507 508 509 510 511 512
            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);
513 514
                }
            } else {
515 516
                $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $this->timeZone, null, $format);
            }
517 518 519 520
            if ($formatter === null) {
                throw new InvalidConfigException(intl_get_error_message());
            }
            return $formatter->format($value);
521
        } else {
522
            if (strncmp($format, 'php:', 4) === 0) {
523
                $format = substr($format, 4);
524
            } else {
525
                $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
526
            }
527 528
            $date = new DateTime(null, new \DateTimeZone($this->timeZone));
            $date->setTimestamp($value);
529 530 531
            return $date->format($format);
        }
    }
532

David Renty committed
533
    /**
534
     * Normalizes the given datetime value as a UNIX timestamp 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
     * @return float the normalized datetime value (int64)
David Renty committed
538
     */
539
    protected function normalizeDatetimeValue($value)
David Renty committed
540
    {
541
        if ($value === null) {
542
            return null;
543
        } elseif (is_string($value)) {
Kartik Visweswaran committed
544 545 546
            if (is_numeric($value) || $value === '') {
                $value = (double)$value;
            } else {
Carsten Brandt committed
547
                $date = new DateTime($value);
Philippe Gaultier committed
548
                $value = (double)$date->format('U');
Kartik Visweswaran committed
549 550
            }
            return $value;
551

Carsten Brandt committed
552
        } elseif ($value instanceof DateTime || $value instanceof DateTimeInterface) {
Philippe Gaultier committed
553
            return (double)$value->format('U');
David Renty committed
554
        } else {
555 556 557 558
            return (double)$value;
        }
    }

559
    /**
560 561 562 563 564 565 566 567 568 569
     * 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.
570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589
     */
    public function asTimestamp($value)
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
        return number_format($this->normalizeDatetimeValue($value), 0, '.', '');
    }

    /**
     * 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)
     *
590 591
     * @param integer|string|DateTime|\DateInterval $referenceTime if specified the value is used instead of `now`.
     * @return string the formatted result.
592
     * @throws InvalidParamException if the input value can not be evaluated as a date value.
593 594 595 596 597 598 599 600 601 602 603 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 631 632 633
     */
    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 {
                    $referenceTime = $this->normalizeDatetimeValue($referenceTime);
                    $dateNow = new DateTime(null, $timezone);
                    $dateNow->setTimestamp($referenceTime);
                }

                $dateThen = new DateTime(null, $timezone);
                $dateThen->setTimestamp($timestamp);

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

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

675 676 677 678

    // number formats


David Renty committed
679
    /**
680 681 682 683 684 685
     * 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.
686
     * @throws InvalidParamException if the input value is not numeric.
David Renty committed
687
     */
688
    public function asInteger($value, $options = [], $textOptions = [])
689
    {
David Renty committed
690 691 692
        if ($value === null) {
            return $this->nullDisplay;
        }
693 694
        $value = $this->normalizeNumericValue($value);
        if ($this->_intlLoaded) {
695
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
696
            return $f->format($value, NumberFormatter::TYPE_INT64);
David Renty committed
697
        } else {
698
            return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
David Renty committed
699 700
        }
    }
701 702

    /**
703 704
     * Formats the value as a decimal number.
     *
705
     * Property [[decimalSeparator]] will be used to represent the decimal point. The
706 707
     * value is rounded automatically to the defined decimal digits.
     *
708
     * @param mixed $value the value to be formatted.
709 710
     * @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`.
711 712 713
     * @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.
714
     * @throws InvalidParamException if the input value is not numeric.
David Renty committed
715
     * @see decimalSeparator
716
     * @see thousandSeparator
David Renty committed
717
     */
718
    public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
David Renty committed
719 720 721 722
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
723
        $value = $this->normalizeNumericValue($value);
724

725
        if ($this->_intlLoaded) {
726
            $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
727
            return $f->format($value);
David Renty committed
728
        } else {
729 730 731
            if ($decimals === null){
                $decimals = 2;
            }
732
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
David Renty committed
733 734 735
        }
    }

736

David Renty committed
737
    /**
738
     * Formats the value as a percent number with "%" sign.
739 740 741 742 743
     *
     * @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]].
744
     * @return string the formatted result.
745
     * @throws InvalidParamException if the input value is not numeric.
746
     */
747
    public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
748 749 750 751
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
752
        $value = $this->normalizeNumericValue($value);
753

754
        if ($this->_intlLoaded) {
755
            $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
756 757
            return $f->format($value);
        } else {
758 759 760
            if ($decimals === null){
                $decimals = 0;
            }
761
            $value = $value * 100;
762
            return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
763 764
        }
    }
765 766

    /**
767
     * Formats the value as a scientific number.
768
     *
769 770 771 772
     * @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]].
773
     * @return string the formatted result.
774
     * @throws InvalidParamException if the input value is not numeric.
775
     */
776
    public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
777
    {
David Renty committed
778 779 780
        if ($value === null) {
            return $this->nullDisplay;
        }
781
        $value = $this->normalizeNumericValue($value);
782

783
        if ($this->_intlLoaded){
784
            $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
785 786
            return $f->format($value);
        } else {
787
            if ($decimals !== null) {
788
                return sprintf("%.{$decimals}E", $value);
789 790 791
            } else {
                return sprintf("%.E", $value);
            }
792
        }
David Renty committed
793
    }
794 795

    /**
796
     * Formats the value as a currency number.
797 798 799 800
     *
     * 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.
     *
801
     * @param mixed $value the value to be formatted.
802
     * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
Carsten Brandt committed
803
     * If null, [[currencyCode]] will be used.
804 805
     * @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]].
806
     * @return string the formatted result.
807
     * @throws InvalidParamException if the input value is not numeric.
808
     * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
809
     */
810
    public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
811 812 813 814
    {
        if ($value === null) {
            return $this->nullDisplay;
        }
815
        $value = $this->normalizeNumericValue($value);
816 817

        if ($this->_intlLoaded) {
818
            $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
Carsten Brandt committed
819 820 821 822 823 824 825
            if ($currency === null) {
                if ($this->currencyCode === null) {
                    $currency = $formatter->getSymbol(NumberFormatter::INTL_CURRENCY_SYMBOL);
                } else {
                    $currency = $this->currencyCode;
                }
            }
826
            return $formatter->formatCurrency($value, $currency);
827
        } else {
Carsten Brandt committed
828 829 830 831 832 833
            if ($currency === null) {
                if ($this->currencyCode === null) {
                    throw new InvalidConfigException('The default currency code for the formatter is not defined.');
                }
                $currency = $this->currencyCode;
            }
834
            return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
835 836
        }
    }
837

838 839
    /**
     * Formats the value as a number spellout.
840 841 842
     *
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
     *
843 844 845
     * @param mixed $value the value to be formatted
     * @return string the formatted result.
     * @throws InvalidParamException if the input value is not numeric.
846
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
847 848 849 850 851 852 853 854 855 856 857
     */
    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 {
858
            throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
859 860
        }
    }
861

862 863
    /**
     * Formats the value as a ordinal value of a number.
864 865 866
     *
     * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
     *
867 868 869
     * @param mixed $value the value to be formatted
     * @return string the formatted result.
     * @throws InvalidParamException if the input value is not numeric.
870
     * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
871 872 873 874 875 876 877 878 879 880 881
     */
    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 {
882
            throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
883 884
        }
    }
885

David Renty committed
886
    /**
Carsten Brandt committed
887 888 889 890 891 892 893 894 895 896 897
     * 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]].
898
     * @return string the formatted result.
899
     * @throws InvalidParamException if the input value is not numeric.
David Renty committed
900
     * @see sizeFormat
Carsten Brandt committed
901
     * @see asSize
David Renty committed
902
     */
Carsten Brandt committed
903
    public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
David Renty committed
904
    {
905 906 907
        if ($value === null) {
            return $this->nullDisplay;
        }
Carsten Brandt committed
908 909 910 911 912

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

        if ($this->sizeFormatBase == 1024) {
            switch ($position) {
913 914 915 916 917 918
                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
919
            }
Carsten Brandt committed
920 921
        } else {
            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} 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
928 929 930
            }
        }
    }
David Renty committed
931

Carsten Brandt committed
932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953
    /**
     * 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);
954

Carsten Brandt committed
955 956
        if ($this->sizeFormatBase == 1024) {
            switch ($position) {
957 958 959 960 961 962
                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
963 964
            }
        } else {
965
            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{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);
972 973
            }
        }
Carsten Brandt committed
974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992
    }

    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);
993

Carsten Brandt committed
994 995 996 997 998
        // no decimals for bytes
        if ($position === 0) {
            $decimals = 0;
        } elseif ($decimals !== null) {
            $value = round($value, $decimals);
David Renty committed
999
        }
Carsten Brandt committed
1000 1001 1002 1003 1004
        // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
        $oldThousandSeparator = $this->thousandSeparator;
        $this->thousandSeparator = '';
        $options[NumberFormatter::GROUPING_USED] = false;
        // format the size value
1005 1006 1007 1008 1009 1010
        $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
1011 1012 1013
        $this->thousandSeparator = $oldThousandSeparator;

        return [$params, $position];
David Renty committed
1014 1015
    }

1016 1017 1018 1019 1020
    /**
     * @param $value
     * @return float
     * @throws InvalidParamException
     */
1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031
    protected function normalizeNumericValue($value)
    {
        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;
    }

1032 1033
    /**
     * Creates a number formatter based on the given type and format.
1034
     *
Carsten Brandt committed
1035
     * You may overide this method to create a number formatter based on patterns.
1036
     *
Carsten Brandt committed
1037
     * @param integer $style the type of the number formatter.
1038 1039
     * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
     *          ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
Carsten Brandt committed
1040 1041 1042
     * @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]].
1043 1044
     * @return NumberFormatter the created formatter instance
     */
1045
    protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
1046
    {
1047 1048 1049
        $formatter = new NumberFormatter($this->locale, $style);

        if ($this->decimalSeparator !== null) {
1050
            $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
1051 1052
        }
        if ($this->thousandSeparator !== null) {
1053
            $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
1054
        }
1055

1056 1057 1058 1059
        if ($decimals !== null) {
            $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
            $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
        }
1060

Carsten Brandt committed
1061
        foreach ($this->numberFormatterOptions as $name => $value) {
1062 1063
            $formatter->setAttribute($name, $value);
        }
Carsten Brandt committed
1064 1065 1066 1067 1068 1069 1070
        foreach ($options as $name => $value) {
            $formatter->setAttribute($name, $value);
        }
        foreach ($this->numberFormatterTextOptions as $name => $attribute) {
            $formatter->setTextAttribute($name, $attribute);
        }
        foreach ($textOptions as $name => $attribute) {
1071 1072
            $formatter->setTextAttribute($name, $attribute);
        }
1073 1074
        return $formatter;
    }
Qiang Xue committed
1075
}