Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

1362 lines
62KB

  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\i18n;
  8. use DateInterval;
  9. use DateTime;
  10. use DateTimeInterface;
  11. use DateTimeZone;
  12. use IntlDateFormatter;
  13. use NumberFormatter;
  14. use Yii;
  15. use yii\base\Component;
  16. use yii\base\InvalidConfigException;
  17. use yii\base\InvalidParamException;
  18. use yii\helpers\FormatConverter;
  19. use yii\helpers\HtmlPurifier;
  20. use yii\helpers\Html;
  21. /**
  22. * Formatter provides a set of commonly used data formatting methods.
  23. *
  24. * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
  25. * The behavior of some of them may be configured via the properties of Formatter. For example,
  26. * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
  27. *
  28. * Formatter is configured as an application component in [[\yii\base\Application]] by default.
  29. * You can access that instance via `Yii::$app->formatter`.
  30. *
  31. * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
  32. * the [PHP intl extension](http://php.net/manual/en/book.intl.php) has to be installed.
  33. * 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. * Note that even if the intl extension is installed, formatting date and time values for years >=2038 or <=1901
  36. * on 32bit systems will fall back to the PHP implementation because intl uses a 32bit UNIX timestamp internally.
  37. * On a 64bit system the intl formatter is used in all cases if installed.
  38. *
  39. * @author Qiang Xue <qiang.xue@gmail.com>
  40. * @author Enrica Ruedin <e.ruedin@guggach.com>
  41. * @author Carsten Brandt <mail@cebe.cc>
  42. * @since 2.0
  43. */
  44. class Formatter extends Component
  45. {
  46. /**
  47. * @var string the text to be displayed when formatting a `null` value.
  48. * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
  49. * will be translated according to [[locale]].
  50. */
  51. public $nullDisplay;
  52. /**
  53. * @var array the text to be displayed when formatting a boolean value. The first element corresponds
  54. * to the text displayed for `false`, the second element for `true`.
  55. * Defaults to `['No', 'Yes']`, where `Yes` and `No`
  56. * will be translated according to [[locale]].
  57. */
  58. public $booleanFormat;
  59. /**
  60. * @var string the locale ID that is used to localize the date and number formatting.
  61. * For number and date formatting this is only effective when the
  62. * [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
  63. * If not set, [[\yii\base\Application::language]] will be used.
  64. */
  65. public $locale;
  66. /**
  67. * @var string the time zone to use for formatting time and date values.
  68. *
  69. * 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)
  70. * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
  71. * Refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
  72. * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
  73. *
  74. * Note that the default time zone for input data is assumed to be UTC by default if no time zone is included in the input date value.
  75. * If you store your data in a different time zone in the database, you have to adjust [[defaultTimeZone]] accordingly.
  76. */
  77. public $timeZone;
  78. /**
  79. * @var string the time zone that is assumed for input values if they do not include a time zone explicitly.
  80. *
  81. * The value must be a valid time zone identifier, e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
  82. * Please refer to the [php manual](http://www.php.net/manual/en/timezones.php) for available time zones.
  83. *
  84. * It defaults to `UTC` so you only have to adjust this value if you store datetime values in another time zone in your database.
  85. *
  86. * @since 2.0.1
  87. */
  88. public $defaultTimeZone = 'UTC';
  89. /**
  90. * @var string the default format string to be used to format a [[asDate()|date]].
  91. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  92. *
  93. * 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).
  94. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  95. * PHP [date()](http://php.net/manual/en/function.date.php)-function.
  96. *
  97. * For example:
  98. *
  99. * ```php
  100. * 'MM/dd/yyyy' // date in ICU format
  101. * 'php:m/d/Y' // the same date in PHP format
  102. * ```
  103. */
  104. public $dateFormat = 'medium';
  105. /**
  106. * @var string the default format string to be used to format a [[asTime()|time]].
  107. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  108. *
  109. * 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).
  110. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  111. * PHP [date()](http://php.net/manual/en/function.date.php)-function.
  112. *
  113. * For example:
  114. *
  115. * ```php
  116. * 'HH:mm:ss' // time in ICU format
  117. * 'php:H:i:s' // the same time in PHP format
  118. * ```
  119. */
  120. public $timeFormat = 'medium';
  121. /**
  122. * @var string the default format string to be used to format a [[asDatetime()|date and time]].
  123. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  124. *
  125. * 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).
  126. *
  127. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  128. * PHP [date()](http://php.net/manual/en/function.date.php)-function.
  129. *
  130. * For example:
  131. *
  132. * ```php
  133. * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
  134. * 'php:m/d/Y H:i:s' // the same date and time in PHP format
  135. * ```
  136. */
  137. public $datetimeFormat = 'medium';
  138. /**
  139. * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
  140. * passed to the [constructor of the `IntlDateFormatter` class](http://php.net/manual/en/intldateformatter.create.php).
  141. *
  142. * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
  143. * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
  144. *
  145. * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
  146. * set this property to `\IntlDateFormatter::TRADITIONAL`.
  147. * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
  148. *
  149. * ```php
  150. * 'formatter' => [
  151. * 'locale' => 'fa_IR@calendar=persian',
  152. * 'calendar' => \IntlDateFormatter::TRADITIONAL,
  153. * ],
  154. * ```
  155. *
  156. * Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar).
  157. *
  158. * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
  159. * Check the [PHP manual](http://php.net/manual/en/intldateformatter.create.php) for more details.
  160. *
  161. * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
  162. *
  163. * @see http://php.net/manual/en/intldateformatter.create.php
  164. * @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
  165. * @see http://php.net/manual/en/class.intlcalendar.php
  166. * @since 2.0.7
  167. */
  168. public $calendar;
  169. /**
  170. * @var string the character displayed as the decimal point when formatting a number.
  171. * If not set, the decimal separator corresponding to [[locale]] will be used.
  172. * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'.
  173. */
  174. public $decimalSeparator;
  175. /**
  176. * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
  177. * If not set, the thousand separator corresponding to [[locale]] will be used.
  178. * If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is ','.
  179. */
  180. public $thousandSeparator;
  181. /**
  182. * @var array a list of name value pairs that are passed to the
  183. * intl [Numberformatter::setAttribute()](http://php.net/manual/en/numberformatter.setattribute.php) method of all
  184. * the number formatter objects created by [[createNumberFormatter()]].
  185. * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
  186. *
  187. * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
  188. * for the possible options.
  189. *
  190. * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
  191. *
  192. * ```php
  193. * [
  194. * NumberFormatter::MIN_FRACTION_DIGITS => 0,
  195. * NumberFormatter::MAX_FRACTION_DIGITS => 2,
  196. * ]
  197. * ```
  198. */
  199. public $numberFormatterOptions = [];
  200. /**
  201. * @var array a list of name value pairs that are passed to the
  202. * intl [Numberformatter::setTextAttribute()](http://php.net/manual/en/numberformatter.settextattribute.php) method of all
  203. * the number formatter objects created by [[createNumberFormatter()]].
  204. * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
  205. *
  206. * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
  207. * for the possible options.
  208. *
  209. * For example to change the minus sign for negative numbers you can configure this property like the following:
  210. *
  211. * ```php
  212. * [
  213. * NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
  214. * ]
  215. * ```
  216. */
  217. public $numberFormatterTextOptions = [];
  218. /**
  219. * @var array a list of name value pairs that are passed to the
  220. * intl [Numberformatter::setSymbol()](http://php.net/manual/en/numberformatter.setsymbol.php) method of all
  221. * the number formatter objects created by [[createNumberFormatter()]].
  222. * This property takes only effect if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is installed.
  223. *
  224. * Please refer to the [PHP manual](http://php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatsymbol)
  225. * for the possible options.
  226. *
  227. * For example to choose a custom currency symbol, e.g. [U+20BD](http://unicode-table.com/en/20BD/) instead of `руб.` for Russian Ruble:
  228. *
  229. * ```php
  230. * [
  231. * NumberFormatter::CURRENCY_SYMBOL => '₽',
  232. * ]
  233. * ```
  234. *
  235. * @since 2.0.4
  236. */
  237. public $numberFormatterSymbols = [];
  238. /**
  239. * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
  240. * If not set, the currency code corresponding to [[locale]] will be used.
  241. * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
  242. * is not possible to determine the default currency.
  243. */
  244. public $currencyCode;
  245. /**
  246. * @var integer the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
  247. * Defaults to 1024.
  248. */
  249. public $sizeFormatBase = 1024;
  250. /**
  251. * @var boolean whether the [PHP intl extension](http://php.net/manual/en/book.intl.php) is loaded.
  252. */
  253. private $_intlLoaded = false;
  254. /**
  255. * @inheritdoc
  256. */
  257. public function init()
  258. {
  259. if ($this->timeZone === null) {
  260. $this->timeZone = Yii::$app->timeZone;
  261. }
  262. if ($this->locale === null) {
  263. $this->locale = Yii::$app->language;
  264. }
  265. if ($this->booleanFormat === null) {
  266. $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
  267. }
  268. if ($this->nullDisplay === null) {
  269. $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
  270. }
  271. $this->_intlLoaded = extension_loaded('intl');
  272. if (!$this->_intlLoaded) {
  273. if ($this->decimalSeparator === null) {
  274. $this->decimalSeparator = '.';
  275. }
  276. if ($this->thousandSeparator === null) {
  277. $this->thousandSeparator = ',';
  278. }
  279. }
  280. }
  281. /**
  282. * Formats the value based on the given format type.
  283. * This method will call one of the "as" methods available in this class to do the formatting.
  284. * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
  285. * then [[asHtml()]] will be used. Format names are case insensitive.
  286. * @param mixed $value the value to be formatted.
  287. * @param string|array $format the format of the value, e.g., "html", "text". To specify additional
  288. * parameters of the formatting method, you may use an array. The first element of the array
  289. * specifies the format name, while the rest of the elements will be used as the parameters to the formatting
  290. * method. For example, a format of `['date', 'Y-m-d']` will cause the invocation of `asDate($value, 'Y-m-d')`.
  291. * @return string the formatting result.
  292. * @throws InvalidParamException if the format type is not supported by this class.
  293. */
  294. public function format($value, $format)
  295. {
  296. if (is_array($format)) {
  297. if (!isset($format[0])) {
  298. throw new InvalidParamException('The $format array must contain at least one element.');
  299. }
  300. $f = $format[0];
  301. $format[0] = $value;
  302. $params = $format;
  303. $format = $f;
  304. } else {
  305. $params = [$value];
  306. }
  307. $method = 'as' . $format;
  308. if ($this->hasMethod($method)) {
  309. return call_user_func_array([$this, $method], $params);
  310. } else {
  311. throw new InvalidParamException("Unknown format type: $format");
  312. }
  313. }
  314. // simple formats
  315. /**
  316. * Formats the value as is without any formatting.
  317. * This method simply returns back the parameter without any format.
  318. * The only exception is a `null` value which will be formatted using [[nullDisplay]].
  319. * @param mixed $value the value to be formatted.
  320. * @return string the formatted result.
  321. */
  322. public function asRaw($value)
  323. {
  324. if ($value === null) {
  325. return $this->nullDisplay;
  326. }
  327. return $value;
  328. }
  329. /**
  330. * Formats the value as an HTML-encoded plain text.
  331. * @param string $value the value to be formatted.
  332. * @return string the formatted result.
  333. */
  334. public function asText($value)
  335. {
  336. if ($value === null) {
  337. return $this->nullDisplay;
  338. }
  339. return Html::encode($value);
  340. }
  341. /**
  342. * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
  343. * @param string $value the value to be formatted.
  344. * @return string the formatted result.
  345. */
  346. public function asNtext($value)
  347. {
  348. if ($value === null) {
  349. return $this->nullDisplay;
  350. }
  351. return nl2br(Html::encode($value));
  352. }
  353. /**
  354. * Formats the value as HTML-encoded text paragraphs.
  355. * Each text paragraph is enclosed within a `<p>` tag.
  356. * One or multiple consecutive empty lines divide two paragraphs.
  357. * @param string $value the value to be formatted.
  358. * @return string the formatted result.
  359. */
  360. public function asParagraphs($value)
  361. {
  362. if ($value === null) {
  363. return $this->nullDisplay;
  364. }
  365. return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/u', "</p>\n<p>", Html::encode($value)) . '</p>');
  366. }
  367. /**
  368. * Formats the value as HTML text.
  369. * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
  370. * Use [[asRaw()]] if you do not want any purification of the value.
  371. * @param string $value the value to be formatted.
  372. * @param array|null $config the configuration for the HTMLPurifier class.
  373. * @return string the formatted result.
  374. */
  375. public function asHtml($value, $config = null)
  376. {
  377. if ($value === null) {
  378. return $this->nullDisplay;
  379. }
  380. return HtmlPurifier::process($value, $config);
  381. }
  382. /**
  383. * Formats the value as a mailto link.
  384. * @param string $value the value to be formatted.
  385. * @param array $options the tag options in terms of name-value pairs. See [[Html::mailto()]].
  386. * @return string the formatted result.
  387. */
  388. public function asEmail($value, $options = [])
  389. {
  390. if ($value === null) {
  391. return $this->nullDisplay;
  392. }
  393. return Html::mailto(Html::encode($value), $value, $options);
  394. }
  395. /**
  396. * Formats the value as an image tag.
  397. * @param mixed $value the value to be formatted.
  398. * @param array $options the tag options in terms of name-value pairs. See [[Html::img()]].
  399. * @return string the formatted result.
  400. */
  401. public function asImage($value, $options = [])
  402. {
  403. if ($value === null) {
  404. return $this->nullDisplay;
  405. }
  406. return Html::img($value, $options);
  407. }
  408. /**
  409. * Formats the value as a hyperlink.
  410. * @param mixed $value the value to be formatted.
  411. * @param array $options the tag options in terms of name-value pairs. See [[Html::a()]].
  412. * @return string the formatted result.
  413. */
  414. public function asUrl($value, $options = [])
  415. {
  416. if ($value === null) {
  417. return $this->nullDisplay;
  418. }
  419. $url = $value;
  420. if (strpos($url, '://') === false) {
  421. $url = 'http://' . $url;
  422. }
  423. return Html::a(Html::encode($value), $url, $options);
  424. }
  425. /**
  426. * Formats the value as a boolean.
  427. * @param mixed $value the value to be formatted.
  428. * @return string the formatted result.
  429. * @see booleanFormat
  430. */
  431. public function asBoolean($value)
  432. {
  433. if ($value === null) {
  434. return $this->nullDisplay;
  435. }
  436. return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
  437. }
  438. // date and time formats
  439. /**
  440. * Formats the value as a date.
  441. * @param integer|string|DateTime $value the value to be formatted. The following
  442. * types of value are supported:
  443. *
  444. * - an integer representing a UNIX timestamp
  445. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  446. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  447. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  448. *
  449. * @param string $format the format used to convert the value into a date string.
  450. * If null, [[dateFormat]] will be used.
  451. *
  452. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  453. * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
  454. *
  455. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  456. * PHP [date()](http://php.net/manual/en/function.date.php)-function.
  457. *
  458. * @return string the formatted result.
  459. * @throws InvalidParamException if the input value can not be evaluated as a date value.
  460. * @throws InvalidConfigException if the date format is invalid.
  461. * @see dateFormat
  462. */
  463. public function asDate($value, $format = null)
  464. {
  465. if ($format === null) {
  466. $format = $this->dateFormat;
  467. }
  468. return $this->formatDateTimeValue($value, $format, 'date');
  469. }
  470. /**
  471. * Formats the value as a time.
  472. * @param integer|string|DateTime $value the value to be formatted. The following
  473. * types of value are supported:
  474. *
  475. * - an integer representing a UNIX timestamp
  476. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  477. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  478. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  479. *
  480. * @param string $format the format used to convert the value into a date string.
  481. * If null, [[timeFormat]] will be used.
  482. *
  483. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  484. * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
  485. *
  486. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  487. * PHP [date()](http://php.net/manual/en/function.date.php)-function.
  488. *
  489. * @return string the formatted result.
  490. * @throws InvalidParamException if the input value can not be evaluated as a date value.
  491. * @throws InvalidConfigException if the date format is invalid.
  492. * @see timeFormat
  493. */
  494. public function asTime($value, $format = null)
  495. {
  496. if ($format === null) {
  497. $format = $this->timeFormat;
  498. }
  499. return $this->formatDateTimeValue($value, $format, 'time');
  500. }
  501. /**
  502. * Formats the value as a datetime.
  503. * @param integer|string|DateTime $value the value to be formatted. The following
  504. * types of value are supported:
  505. *
  506. * - an integer representing a UNIX timestamp
  507. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  508. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  509. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  510. *
  511. * @param string $format the format used to convert the value into a date string.
  512. * If null, [[dateFormat]] will be used.
  513. *
  514. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  515. * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
  516. *
  517. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  518. * PHP [date()](http://php.net/manual/en/function.date.php)-function.
  519. *
  520. * @return string the formatted result.
  521. * @throws InvalidParamException if the input value can not be evaluated as a date value.
  522. * @throws InvalidConfigException if the date format is invalid.
  523. * @see datetimeFormat
  524. */
  525. public function asDatetime($value, $format = null)
  526. {
  527. if ($format === null) {
  528. $format = $this->datetimeFormat;
  529. }
  530. return $this->formatDateTimeValue($value, $format, 'datetime');
  531. }
  532. /**
  533. * @var array map of short format names to IntlDateFormatter constant values.
  534. */
  535. private $_dateFormats = [
  536. 'short' => 3, // IntlDateFormatter::SHORT,
  537. 'medium' => 2, // IntlDateFormatter::MEDIUM,
  538. 'long' => 1, // IntlDateFormatter::LONG,
  539. 'full' => 0, // IntlDateFormatter::FULL,
  540. ];
  541. /**
  542. * @param integer|string|DateTime $value the value to be formatted. The following
  543. * types of value are supported:
  544. *
  545. * - an integer representing a UNIX timestamp
  546. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  547. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  548. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  549. *
  550. * @param string $format the format used to convert the value into a date string.
  551. * @param string $type 'date', 'time', or 'datetime'.
  552. * @throws InvalidConfigException if the date format is invalid.
  553. * @return string the formatted result.
  554. */
  555. private function formatDateTimeValue($value, $format, $type)
  556. {
  557. $timeZone = $this->timeZone;
  558. // avoid time zone conversion for date-only values
  559. if ($type === 'date') {
  560. list($timestamp, $hasTimeInfo) = $this->normalizeDatetimeValue($value, true);
  561. if (!$hasTimeInfo) {
  562. $timeZone = $this->defaultTimeZone;
  563. }
  564. } else {
  565. $timestamp = $this->normalizeDatetimeValue($value);
  566. }
  567. if ($timestamp === null) {
  568. return $this->nullDisplay;
  569. }
  570. // intl does not work with dates >=2038 or <=1901 on 32bit machines, fall back to PHP
  571. $year = $timestamp->format('Y');
  572. if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
  573. if (strncmp($format, 'php:', 4) === 0) {
  574. $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
  575. }
  576. if (isset($this->_dateFormats[$format])) {
  577. if ($type === 'date') {
  578. $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar);
  579. } elseif ($type === 'time') {
  580. $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar);
  581. } else {
  582. $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar);
  583. }
  584. } else {
  585. $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
  586. }
  587. if ($formatter === null) {
  588. throw new InvalidConfigException(intl_get_error_message());
  589. }
  590. // make IntlDateFormatter work with DateTimeImmutable
  591. if ($timestamp instanceof \DateTimeImmutable) {
  592. $timestamp = new DateTime($timestamp->format(DateTime::ISO8601), $timestamp->getTimezone());
  593. }
  594. return $formatter->format($timestamp);
  595. } else {
  596. if (strncmp($format, 'php:', 4) === 0) {
  597. $format = substr($format, 4);
  598. } else {
  599. $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
  600. }
  601. if ($timeZone != null) {
  602. if ($timestamp instanceof \DateTimeImmutable) {
  603. $timestamp = $timestamp->setTimezone(new DateTimeZone($timeZone));
  604. } else {
  605. $timestamp->setTimezone(new DateTimeZone($timeZone));
  606. }
  607. }
  608. return $timestamp->format($format);
  609. }
  610. }
  611. /**
  612. * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
  613. *
  614. * @param integer|string|DateTime $value the datetime value to be normalized. The following
  615. * types of value are supported:
  616. *
  617. * - an integer representing a UNIX timestamp
  618. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  619. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  620. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  621. *
  622. * @param boolean $checkTimeInfo whether to also check if the date/time value has some time information attached.
  623. * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
  624. * timestamp and the second a boolean indicating whether the timestamp has time information or it is just a date value.
  625. * This parameter is available since version 2.0.1.
  626. * @return DateTime|array the normalized datetime value.
  627. * Since version 2.0.1 this may also return an array if `$checkTimeInfo` is true.
  628. * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
  629. * the timestamp has time information or it is just a date value.
  630. * @throws InvalidParamException if the input value can not be evaluated as a date value.
  631. */
  632. protected function normalizeDatetimeValue($value, $checkTimeInfo = false)
  633. {
  634. // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
  635. if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
  636. // skip any processing
  637. return $checkTimeInfo ? [$value, true] : $value;
  638. }
  639. if (empty($value)) {
  640. $value = 0;
  641. }
  642. try {
  643. if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
  644. $timestamp = new DateTime('@' . $value, new DateTimeZone('UTC'));
  645. return $checkTimeInfo ? [$timestamp, true] : $timestamp;
  646. } elseif (($timestamp = DateTime::createFromFormat('Y-m-d', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d format (support invalid dates like 2012-13-01)
  647. return $checkTimeInfo ? [$timestamp, false] : $timestamp;
  648. } elseif (($timestamp = DateTime::createFromFormat('Y-m-d H:i:s', $value, new DateTimeZone($this->defaultTimeZone))) !== false) { // try Y-m-d H:i:s format (support invalid dates like 2012-13-01 12:63:12)
  649. return $checkTimeInfo ? [$timestamp, true] : $timestamp;
  650. }
  651. // finally try to create a DateTime object with the value
  652. if ($checkTimeInfo) {
  653. $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
  654. $info = date_parse($value);
  655. return [$timestamp, !($info['hour'] === false && $info['minute'] === false && $info['second'] === false)];
  656. } else {
  657. return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
  658. }
  659. } catch (\Exception $e) {
  660. throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
  661. . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
  662. }
  663. }
  664. /**
  665. * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
  666. * @param integer|string|DateTime $value the value to be formatted. The following
  667. * types of value are supported:
  668. *
  669. * - an integer representing a UNIX timestamp
  670. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  671. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  672. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  673. *
  674. * @return string the formatted result.
  675. */
  676. public function asTimestamp($value)
  677. {
  678. if ($value === null) {
  679. return $this->nullDisplay;
  680. }
  681. $timestamp = $this->normalizeDatetimeValue($value);
  682. return number_format($timestamp->format('U'), 0, '.', '');
  683. }
  684. /**
  685. * Formats the value as the time interval between a date and now in human readable form.
  686. *
  687. * This method can be used in three different ways:
  688. *
  689. * 1. Using a timestamp that is relative to `now`.
  690. * 2. Using a timestamp that is relative to the `$referenceTime`.
  691. * 3. Using a `DateInterval` object.
  692. *
  693. * @param integer|string|DateTime|DateInterval $value the value to be formatted. The following
  694. * types of value are supported:
  695. *
  696. * - an integer representing a UNIX timestamp
  697. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  698. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  699. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  700. * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
  701. *
  702. * @param integer|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
  703. * when `$value` is not a `DateInterval` object.
  704. * @return string the formatted result.
  705. * @throws InvalidParamException if the input value can not be evaluated as a date value.
  706. */
  707. public function asRelativeTime($value, $referenceTime = null)
  708. {
  709. if ($value === null) {
  710. return $this->nullDisplay;
  711. }
  712. if ($value instanceof DateInterval) {
  713. $interval = $value;
  714. } else {
  715. $timestamp = $this->normalizeDatetimeValue($value);
  716. if ($timestamp === false) {
  717. // $value is not a valid date/time value, so we try
  718. // to create a DateInterval with it
  719. try {
  720. $interval = new DateInterval($value);
  721. } catch (\Exception $e) {
  722. // invalid date/time and invalid interval
  723. return $this->nullDisplay;
  724. }
  725. } else {
  726. $timeZone = new DateTimeZone($this->timeZone);
  727. if ($referenceTime === null) {
  728. $dateNow = new DateTime('now', $timeZone);
  729. } else {
  730. $dateNow = $this->normalizeDatetimeValue($referenceTime);
  731. $dateNow->setTimezone($timeZone);
  732. }
  733. $dateThen = $timestamp->setTimezone($timeZone);
  734. $interval = $dateThen->diff($dateNow);
  735. }
  736. }
  737. if ($interval->invert) {
  738. if ($interval->y >= 1) {
  739. return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
  740. }
  741. if ($interval->m >= 1) {
  742. return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
  743. }
  744. if ($interval->d >= 1) {
  745. return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
  746. }
  747. if ($interval->h >= 1) {
  748. return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
  749. }
  750. if ($interval->i >= 1) {
  751. return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
  752. }
  753. if ($interval->s == 0) {
  754. return Yii::t('yii', 'just now', [], $this->locale);
  755. }
  756. return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  757. } else {
  758. if ($interval->y >= 1) {
  759. return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
  760. }
  761. if ($interval->m >= 1) {
  762. return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
  763. }
  764. if ($interval->d >= 1) {
  765. return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
  766. }
  767. if ($interval->h >= 1) {
  768. return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
  769. }
  770. if ($interval->i >= 1) {
  771. return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
  772. }
  773. if ($interval->s == 0) {
  774. return Yii::t('yii', 'just now', [], $this->locale);
  775. }
  776. return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
  777. }
  778. }
  779. /**
  780. * Represents the value as duration in human readable format.
  781. *
  782. * @param DateInterval|string|integer $value the value to be formatted. Acceptable formats:
  783. * - [DateInterval object](http://php.net/manual/ru/class.dateinterval.php)
  784. * - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
  785. * - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
  786. * `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
  787. * `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
  788. * `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
  789. * `P1D2H30M` - simply a date interval
  790. * `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
  791. *
  792. * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
  793. * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
  794. * @return string the formatted duration.
  795. * @since 2.0.7
  796. */
  797. public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
  798. {
  799. if ($value === null) {
  800. return $this->nullDisplay;
  801. }
  802. if ($value instanceof DateInterval) {
  803. $isNegative = $value->invert;
  804. $interval = $value;
  805. } elseif (is_numeric($value)) {
  806. $isNegative = $value < 0;
  807. $zeroDateTime = (new DateTime())->setTimestamp(0);
  808. $valueDateTime = (new DateTime())->setTimestamp(abs($value));
  809. $interval = $valueDateTime->diff($zeroDateTime);
  810. } elseif (strpos($value, 'P-') === 0) {
  811. $interval = new DateInterval('P'.substr($value, 2));
  812. $isNegative = true;
  813. } else {
  814. $interval = new DateInterval($value);
  815. $isNegative = $interval->invert;
  816. }
  817. if ($interval->y > 0) {
  818. $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->locale);
  819. }
  820. if ($interval->m > 0) {
  821. $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->locale);
  822. }
  823. if ($interval->d > 0) {
  824. $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale);
  825. }
  826. if ($interval->h > 0) {
  827. $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
  828. }
  829. if ($interval->i > 0) {
  830. $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
  831. }
  832. if ($interval->s > 0) {
  833. $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  834. }
  835. if ($interval->s === 0 && empty($parts)) {
  836. $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  837. $isNegative = false;
  838. }
  839. return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
  840. }
  841. // number formats
  842. /**
  843. * Formats the value as an integer number by removing any decimal digits without rounding.
  844. *
  845. * @param mixed $value the value to be formatted.
  846. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  847. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  848. * @return string the formatted result.
  849. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  850. */
  851. public function asInteger($value, $options = [], $textOptions = [])
  852. {
  853. if ($value === null) {
  854. return $this->nullDisplay;
  855. }
  856. $value = $this->normalizeNumericValue($value);
  857. if ($this->_intlLoaded) {
  858. $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
  859. $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
  860. if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
  861. throw new InvalidParamException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  862. }
  863. return $result;
  864. } else {
  865. return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
  866. }
  867. }
  868. /**
  869. * Formats the value as a decimal number.
  870. *
  871. * Property [[decimalSeparator]] will be used to represent the decimal point. The
  872. * value is rounded automatically to the defined decimal digits.
  873. *
  874. * @param mixed $value the value to be formatted.
  875. * @param integer $decimals the number of digits after the decimal point. If not given the number of digits is determined from the
  876. * [[locale]] and if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available defaults to `2`.
  877. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  878. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  879. * @return string the formatted result.
  880. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  881. * @see decimalSeparator
  882. * @see thousandSeparator
  883. */
  884. public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
  885. {
  886. if ($value === null) {
  887. return $this->nullDisplay;
  888. }
  889. $value = $this->normalizeNumericValue($value);
  890. if ($this->_intlLoaded) {
  891. $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
  892. if (($result = $f->format($value)) === false) {
  893. throw new InvalidParamException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  894. }
  895. return $result;
  896. } else {
  897. if ($decimals === null) {
  898. $decimals = 2;
  899. }
  900. return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
  901. }
  902. }
  903. /**
  904. * Formats the value as a percent number with "%" sign.
  905. *
  906. * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
  907. * @param integer $decimals the number of digits after the decimal point.
  908. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  909. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  910. * @return string the formatted result.
  911. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  912. */
  913. public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
  914. {
  915. if ($value === null) {
  916. return $this->nullDisplay;
  917. }
  918. $value = $this->normalizeNumericValue($value);
  919. if ($this->_intlLoaded) {
  920. $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
  921. if (($result = $f->format($value)) === false) {
  922. throw new InvalidParamException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  923. }
  924. return $result;
  925. } else {
  926. if ($decimals === null) {
  927. $decimals = 0;
  928. }
  929. $value *= 100;
  930. return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
  931. }
  932. }
  933. /**
  934. * Formats the value as a scientific number.
  935. *
  936. * @param mixed $value the value to be formatted.
  937. * @param integer $decimals the number of digits after the decimal point.
  938. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  939. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  940. * @return string the formatted result.
  941. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  942. */
  943. public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
  944. {
  945. if ($value === null) {
  946. return $this->nullDisplay;
  947. }
  948. $value = $this->normalizeNumericValue($value);
  949. if ($this->_intlLoaded) {
  950. $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
  951. if (($result = $f->format($value)) === false) {
  952. throw new InvalidParamException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  953. }
  954. return $result;
  955. } else {
  956. if ($decimals !== null) {
  957. return sprintf("%.{$decimals}E", $value);
  958. } else {
  959. return sprintf('%.E', $value);
  960. }
  961. }
  962. }
  963. /**
  964. * Formats the value as a currency number.
  965. *
  966. * This function does not require the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed
  967. * to work, but it is highly recommended to install it to get good formatting results.
  968. *
  969. * @param mixed $value the value to be formatted.
  970. * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
  971. * If null, [[currencyCode]] will be used.
  972. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  973. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  974. * @return string the formatted result.
  975. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  976. * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
  977. */
  978. public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
  979. {
  980. if ($value === null) {
  981. return $this->nullDisplay;
  982. }
  983. $value = $this->normalizeNumericValue($value);
  984. if ($this->_intlLoaded) {
  985. $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
  986. if ($currency === null) {
  987. if ($this->currencyCode === null) {
  988. if (($result = $formatter->format($value)) === false) {
  989. throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
  990. }
  991. return $result;
  992. }
  993. $currency = $this->currencyCode;
  994. }
  995. if (($result = $formatter->formatCurrency($value, $currency)) === false) {
  996. throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
  997. }
  998. return $result;
  999. } else {
  1000. if ($currency === null) {
  1001. if ($this->currencyCode === null) {
  1002. throw new InvalidConfigException('The default currency code for the formatter is not defined and the php intl extension is not installed which could take the default currency from the locale.');
  1003. }
  1004. $currency = $this->currencyCode;
  1005. }
  1006. return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
  1007. }
  1008. }
  1009. /**
  1010. * Formats the value as a number spellout.
  1011. *
  1012. * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
  1013. *
  1014. * @param mixed $value the value to be formatted
  1015. * @return string the formatted result.
  1016. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1017. * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
  1018. */
  1019. public function asSpellout($value)
  1020. {
  1021. if ($value === null) {
  1022. return $this->nullDisplay;
  1023. }
  1024. $value = $this->normalizeNumericValue($value);
  1025. if ($this->_intlLoaded) {
  1026. $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
  1027. if (($result = $f->format($value)) === false) {
  1028. throw new InvalidParamException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1029. }
  1030. return $result;
  1031. } else {
  1032. throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
  1033. }
  1034. }
  1035. /**
  1036. * Formats the value as a ordinal value of a number.
  1037. *
  1038. * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
  1039. *
  1040. * @param mixed $value the value to be formatted
  1041. * @return string the formatted result.
  1042. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1043. * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
  1044. */
  1045. public function asOrdinal($value)
  1046. {
  1047. if ($value === null) {
  1048. return $this->nullDisplay;
  1049. }
  1050. $value = $this->normalizeNumericValue($value);
  1051. if ($this->_intlLoaded) {
  1052. $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
  1053. if (($result = $f->format($value)) === false) {
  1054. throw new InvalidParamException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1055. }
  1056. return $result;
  1057. } else {
  1058. throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
  1059. }
  1060. }
  1061. /**
  1062. * Formats the value in bytes as a size in human readable form for example `12 KB`.
  1063. *
  1064. * This is the short form of [[asSize]].
  1065. *
  1066. * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
  1067. * are used in the formatting result.
  1068. *
  1069. * @param string|integer|float $value value in bytes to be formatted.
  1070. * @param integer $decimals the number of digits after the decimal point.
  1071. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1072. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1073. * @return string the formatted result.
  1074. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1075. * @see sizeFormat
  1076. * @see asSize
  1077. */
  1078. public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
  1079. {
  1080. if ($value === null) {
  1081. return $this->nullDisplay;
  1082. }
  1083. list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
  1084. if ($this->sizeFormatBase == 1024) {
  1085. switch ($position) {
  1086. case 0:
  1087. return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
  1088. case 1:
  1089. return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
  1090. case 2:
  1091. return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
  1092. case 3:
  1093. return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
  1094. case 4:
  1095. return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
  1096. default:
  1097. return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
  1098. }
  1099. } else {
  1100. switch ($position) {
  1101. case 0:
  1102. return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
  1103. case 1:
  1104. return Yii::t('yii', '{nFormatted} KB', $params, $this->locale);
  1105. case 2:
  1106. return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
  1107. case 3:
  1108. return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
  1109. case 4:
  1110. return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
  1111. default:
  1112. return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
  1113. }
  1114. }
  1115. }
  1116. /**
  1117. * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
  1118. *
  1119. * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
  1120. * are used in the formatting result.
  1121. *
  1122. * @param string|integer|float $value value in bytes to be formatted.
  1123. * @param integer $decimals the number of digits after the decimal point.
  1124. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1125. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1126. * @return string the formatted result.
  1127. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1128. * @see sizeFormat
  1129. * @see asShortSize
  1130. */
  1131. public function asSize($value, $decimals = null, $options = [], $textOptions = [])
  1132. {
  1133. if ($value === null) {
  1134. return $this->nullDisplay;
  1135. }
  1136. list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
  1137. if ($this->sizeFormatBase == 1024) {
  1138. switch ($position) {
  1139. case 0:
  1140. return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
  1141. case 1:
  1142. return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
  1143. case 2:
  1144. return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
  1145. case 3:
  1146. return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
  1147. case 4:
  1148. return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
  1149. default:
  1150. return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
  1151. }
  1152. } else {
  1153. switch ($position) {
  1154. case 0:
  1155. return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
  1156. case 1:
  1157. return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
  1158. case 2:
  1159. return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
  1160. case 3:
  1161. return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
  1162. case 4:
  1163. return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
  1164. default:
  1165. return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
  1166. }
  1167. }
  1168. }
  1169. /**
  1170. * Given the value in bytes formats number part of the human readable form.
  1171. *
  1172. * @param string|integer|float $value value in bytes to be formatted.
  1173. * @param integer $decimals the number of digits after the decimal point
  1174. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1175. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1176. * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
  1177. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1178. */
  1179. private function formatSizeNumber($value, $decimals, $options, $textOptions)
  1180. {
  1181. $value = $this->normalizeNumericValue($value);
  1182. $position = 0;
  1183. do {
  1184. if (abs($value) < $this->sizeFormatBase) {
  1185. break;
  1186. }
  1187. $value /= $this->sizeFormatBase;
  1188. $position++;
  1189. } while ($position < 5);
  1190. // no decimals for bytes
  1191. if ($position === 0) {
  1192. $decimals = 0;
  1193. } elseif ($decimals !== null) {
  1194. $value = round($value, $decimals);
  1195. }
  1196. // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
  1197. $oldThousandSeparator = $this->thousandSeparator;
  1198. $this->thousandSeparator = '';
  1199. if ($this->_intlLoaded) {
  1200. $options[NumberFormatter::GROUPING_USED] = false;
  1201. }
  1202. // format the size value
  1203. $params = [
  1204. // this is the unformatted number used for the plural rule
  1205. // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
  1206. // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
  1207. 'n' => abs($value),
  1208. // this is the formatted number used for display
  1209. 'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
  1210. ];
  1211. $this->thousandSeparator = $oldThousandSeparator;
  1212. return [$params, $position];
  1213. }
  1214. /**
  1215. * Normalizes a numeric input value
  1216. *
  1217. * - everything [empty](http://php.net/manual/en/function.empty.php) will result in `0`
  1218. * - a [numeric](http://php.net/manual/en/function.is-numeric.php) string will be casted to float
  1219. * - everything else will be returned if it is [numeric](http://php.net/manual/en/function.is-numeric.php),
  1220. * otherwise an exception is thrown.
  1221. *
  1222. * @param mixed $value the input value
  1223. * @return float|integer the normalized number value
  1224. * @throws InvalidParamException if the input value is not numeric.
  1225. */
  1226. protected function normalizeNumericValue($value)
  1227. {
  1228. if (empty($value)) {
  1229. return 0;
  1230. }
  1231. if (is_string($value) && is_numeric($value)) {
  1232. $value = (float) $value;
  1233. }
  1234. if (!is_numeric($value)) {
  1235. throw new InvalidParamException("'$value' is not a numeric value.");
  1236. }
  1237. return $value;
  1238. }
  1239. /**
  1240. * Creates a number formatter based on the given type and format.
  1241. *
  1242. * You may override this method to create a number formatter based on patterns.
  1243. *
  1244. * @param integer $style the type of the number formatter.
  1245. * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
  1246. * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
  1247. * @param integer $decimals the number of digits after the decimal point.
  1248. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1249. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1250. * @return NumberFormatter the created formatter instance
  1251. */
  1252. protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
  1253. {
  1254. $formatter = new NumberFormatter($this->locale, $style);
  1255. // set text attributes
  1256. foreach ($this->numberFormatterTextOptions as $name => $attribute) {
  1257. $formatter->setTextAttribute($name, $attribute);
  1258. }
  1259. foreach ($textOptions as $name => $attribute) {
  1260. $formatter->setTextAttribute($name, $attribute);
  1261. }
  1262. // set attributes
  1263. foreach ($this->numberFormatterOptions as $name => $value) {
  1264. $formatter->setAttribute($name, $value);
  1265. }
  1266. foreach ($options as $name => $value) {
  1267. $formatter->setAttribute($name, $value);
  1268. }
  1269. if ($decimals !== null) {
  1270. $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
  1271. $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
  1272. }
  1273. // set symbols
  1274. if ($this->decimalSeparator !== null) {
  1275. $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
  1276. }
  1277. if ($this->thousandSeparator !== null) {
  1278. $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
  1279. $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
  1280. }
  1281. foreach ($this->numberFormatterSymbols as $name => $symbol) {
  1282. $formatter->setSymbol($name, $symbol);
  1283. }
  1284. return $formatter;
  1285. }
  1286. }