Formatter.php 64 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387
  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 int 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 bool 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 int|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 int|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 int|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 int|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 and time-only values
  559. if ($type === 'date' || $type === 'time') {
  560. list($timestamp, $hasTimeInfo, $hasDateInfo) = $this->normalizeDatetimeValue($value, true);
  561. if ($type === 'date' && !$hasTimeInfo || $type === 'time' && !$hasDateInfo) {
  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 int|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 bool $checkDateTimeInfo whether to also check if the date/time value has some time and date information attached.
  623. * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
  624. * timestamp, the second a boolean indicating whether the timestamp has time information and third a boolean indicating
  625. * whether the timestamp has date information.
  626. * This parameter is available since version 2.0.1.
  627. * @return DateTime|array the normalized datetime value.
  628. * Since version 2.0.1 this may also return an array if `$checkTimeInfo` is true.
  629. * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
  630. * the timestamp has time information or it is just a date value.
  631. * Since version 2.0.12 the array has third boolean element indicating whether the timestamp has date information
  632. * or it is just a time value.
  633. * @throws InvalidParamException if the input value can not be evaluated as a date value.
  634. */
  635. protected function normalizeDatetimeValue($value, $checkDateTimeInfo = false)
  636. {
  637. // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
  638. if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
  639. // skip any processing
  640. return $checkDateTimeInfo ? [$value, true, true] : $value;
  641. }
  642. if (empty($value)) {
  643. $value = 0;
  644. }
  645. try {
  646. if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
  647. $timestamp = new DateTime('@' . (int)$value, new DateTimeZone('UTC'));
  648. return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
  649. } 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)
  650. return $checkDateTimeInfo ? [$timestamp, false, true] : $timestamp;
  651. } 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)
  652. return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
  653. }
  654. // finally try to create a DateTime object with the value
  655. if ($checkDateTimeInfo) {
  656. $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
  657. $info = date_parse($value);
  658. return [
  659. $timestamp,
  660. !($info['hour'] === false && $info['minute'] === false && $info['second'] === false),
  661. !($info['year'] === false && $info['month'] === false && $info['day'] === false)
  662. ];
  663. } else {
  664. return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
  665. }
  666. } catch (\Exception $e) {
  667. throw new InvalidParamException("'$value' is not a valid date time value: " . $e->getMessage()
  668. . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
  669. }
  670. }
  671. /**
  672. * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
  673. * @param int|string|DateTime $value the value to be formatted. The following
  674. * types of value are supported:
  675. *
  676. * - an integer representing a UNIX timestamp
  677. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  678. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  679. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  680. *
  681. * @return string the formatted result.
  682. */
  683. public function asTimestamp($value)
  684. {
  685. if ($value === null) {
  686. return $this->nullDisplay;
  687. }
  688. $timestamp = $this->normalizeDatetimeValue($value);
  689. return number_format($timestamp->format('U'), 0, '.', '');
  690. }
  691. /**
  692. * Formats the value as the time interval between a date and now in human readable form.
  693. *
  694. * This method can be used in three different ways:
  695. *
  696. * 1. Using a timestamp that is relative to `now`.
  697. * 2. Using a timestamp that is relative to the `$referenceTime`.
  698. * 3. Using a `DateInterval` object.
  699. *
  700. * @param int|string|DateTime|DateInterval $value the value to be formatted. The following
  701. * types of value are supported:
  702. *
  703. * - an integer representing a UNIX timestamp
  704. * - a string that can be [parsed to create a DateTime object](http://php.net/manual/en/datetime.formats.php).
  705. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  706. * - a PHP [DateTime](http://php.net/manual/en/class.datetime.php) object
  707. * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
  708. *
  709. * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
  710. * when `$value` is not a `DateInterval` object.
  711. * @return string the formatted result.
  712. * @throws InvalidParamException if the input value can not be evaluated as a date value.
  713. */
  714. public function asRelativeTime($value, $referenceTime = null)
  715. {
  716. if ($value === null) {
  717. return $this->nullDisplay;
  718. }
  719. if ($value instanceof DateInterval) {
  720. $interval = $value;
  721. } else {
  722. $timestamp = $this->normalizeDatetimeValue($value);
  723. if ($timestamp === false) {
  724. // $value is not a valid date/time value, so we try
  725. // to create a DateInterval with it
  726. try {
  727. $interval = new DateInterval($value);
  728. } catch (\Exception $e) {
  729. // invalid date/time and invalid interval
  730. return $this->nullDisplay;
  731. }
  732. } else {
  733. $timeZone = new DateTimeZone($this->timeZone);
  734. if ($referenceTime === null) {
  735. $dateNow = new DateTime('now', $timeZone);
  736. } else {
  737. $dateNow = $this->normalizeDatetimeValue($referenceTime);
  738. $dateNow->setTimezone($timeZone);
  739. }
  740. $dateThen = $timestamp->setTimezone($timeZone);
  741. $interval = $dateThen->diff($dateNow);
  742. }
  743. }
  744. if ($interval->invert) {
  745. if ($interval->y >= 1) {
  746. return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
  747. }
  748. if ($interval->m >= 1) {
  749. return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
  750. }
  751. if ($interval->d >= 1) {
  752. return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
  753. }
  754. if ($interval->h >= 1) {
  755. return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
  756. }
  757. if ($interval->i >= 1) {
  758. return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
  759. }
  760. if ($interval->s == 0) {
  761. return Yii::t('yii', 'just now', [], $this->locale);
  762. }
  763. return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  764. } else {
  765. if ($interval->y >= 1) {
  766. return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
  767. }
  768. if ($interval->m >= 1) {
  769. return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
  770. }
  771. if ($interval->d >= 1) {
  772. return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
  773. }
  774. if ($interval->h >= 1) {
  775. return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
  776. }
  777. if ($interval->i >= 1) {
  778. return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
  779. }
  780. if ($interval->s == 0) {
  781. return Yii::t('yii', 'just now', [], $this->locale);
  782. }
  783. return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
  784. }
  785. }
  786. /**
  787. * Represents the value as duration in human readable format.
  788. *
  789. * @param DateInterval|string|int $value the value to be formatted. Acceptable formats:
  790. * - [DateInterval object](http://php.net/manual/ru/class.dateinterval.php)
  791. * - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
  792. * - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
  793. * `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
  794. * `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
  795. * `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
  796. * `P1D2H30M` - simply a date interval
  797. * `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
  798. *
  799. * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
  800. * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
  801. * @return string the formatted duration.
  802. * @since 2.0.7
  803. */
  804. public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
  805. {
  806. if ($value === null) {
  807. return $this->nullDisplay;
  808. }
  809. if ($value instanceof DateInterval) {
  810. $isNegative = $value->invert;
  811. $interval = $value;
  812. } elseif (is_numeric($value)) {
  813. $isNegative = $value < 0;
  814. $zeroDateTime = (new DateTime())->setTimestamp(0);
  815. $valueDateTime = (new DateTime())->setTimestamp(abs($value));
  816. $interval = $valueDateTime->diff($zeroDateTime);
  817. } elseif (strpos($value, 'P-') === 0) {
  818. $interval = new DateInterval('P'.substr($value, 2));
  819. $isNegative = true;
  820. } else {
  821. $interval = new DateInterval($value);
  822. $isNegative = $interval->invert;
  823. }
  824. if ($interval->y > 0) {
  825. $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->locale);
  826. }
  827. if ($interval->m > 0) {
  828. $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->locale);
  829. }
  830. if ($interval->d > 0) {
  831. $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale);
  832. }
  833. if ($interval->h > 0) {
  834. $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
  835. }
  836. if ($interval->i > 0) {
  837. $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
  838. }
  839. if ($interval->s > 0) {
  840. $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  841. }
  842. if ($interval->s === 0 && empty($parts)) {
  843. $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  844. $isNegative = false;
  845. }
  846. return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
  847. }
  848. // number formats
  849. /**
  850. * Formats the value as an integer number by removing any decimal digits without rounding.
  851. *
  852. * @param mixed $value the value to be formatted.
  853. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  854. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  855. * @return string the formatted result.
  856. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  857. */
  858. public function asInteger($value, $options = [], $textOptions = [])
  859. {
  860. if ($value === null) {
  861. return $this->nullDisplay;
  862. }
  863. $value = $this->normalizeNumericValue($value);
  864. if ($this->_intlLoaded) {
  865. $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
  866. $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
  867. if (($result = $f->format($value, NumberFormatter::TYPE_INT64)) === false) {
  868. throw new InvalidParamException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  869. }
  870. return $result;
  871. } else {
  872. return number_format((int) $value, 0, $this->decimalSeparator, $this->thousandSeparator);
  873. }
  874. }
  875. /**
  876. * Formats the value as a decimal number.
  877. *
  878. * Property [[decimalSeparator]] will be used to represent the decimal point. The
  879. * value is rounded automatically to the defined decimal digits.
  880. *
  881. * @param mixed $value the value to be formatted.
  882. * @param int $decimals the number of digits after the decimal point.
  883. * If not given, the number of digits depends in the input value and is determined based on
  884. * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
  885. * using [[$numberFormatterOptions]].
  886. * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `2`.
  887. * If you want consistent behavior between environments where intl is available and not, you should explicitly
  888. * specify a value here.
  889. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  890. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  891. * @return string the formatted result.
  892. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  893. * @see decimalSeparator
  894. * @see thousandSeparator
  895. */
  896. public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
  897. {
  898. if ($value === null) {
  899. return $this->nullDisplay;
  900. }
  901. $value = $this->normalizeNumericValue($value);
  902. if ($this->_intlLoaded) {
  903. $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
  904. if (($result = $f->format($value)) === false) {
  905. throw new InvalidParamException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  906. }
  907. return $result;
  908. } else {
  909. if ($decimals === null) {
  910. $decimals = 2;
  911. }
  912. return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator);
  913. }
  914. }
  915. /**
  916. * Formats the value as a percent number with "%" sign.
  917. *
  918. * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
  919. * @param int $decimals the number of digits after the decimal point.
  920. * If not given, the number of digits depends in the input value and is determined based on
  921. * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
  922. * using [[$numberFormatterOptions]].
  923. * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `0`.
  924. * If you want consistent behavior between environments where intl is available and not, you should explicitly
  925. * specify a value here.
  926. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  927. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  928. * @return string the formatted result.
  929. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  930. */
  931. public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
  932. {
  933. if ($value === null) {
  934. return $this->nullDisplay;
  935. }
  936. $value = $this->normalizeNumericValue($value);
  937. if ($this->_intlLoaded) {
  938. $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
  939. if (($result = $f->format($value)) === false) {
  940. throw new InvalidParamException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  941. }
  942. return $result;
  943. } else {
  944. if ($decimals === null) {
  945. $decimals = 0;
  946. }
  947. $value *= 100;
  948. return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
  949. }
  950. }
  951. /**
  952. * Formats the value as a scientific number.
  953. *
  954. * @param mixed $value the value to be formatted.
  955. * @param int $decimals the number of digits after the decimal point.
  956. * If not given, the number of digits depends in the input value and is determined based on
  957. * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
  958. * using [[$numberFormatterOptions]].
  959. * If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value depends on your PHP configuration.
  960. * If you want consistent behavior between environments where intl is available and not, you should explicitly
  961. * specify a value here.
  962. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  963. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  964. * @return string the formatted result.
  965. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  966. */
  967. public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
  968. {
  969. if ($value === null) {
  970. return $this->nullDisplay;
  971. }
  972. $value = $this->normalizeNumericValue($value);
  973. if ($this->_intlLoaded) {
  974. $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
  975. if (($result = $f->format($value)) === false) {
  976. throw new InvalidParamException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  977. }
  978. return $result;
  979. } else {
  980. if ($decimals !== null) {
  981. return sprintf("%.{$decimals}E", $value);
  982. } else {
  983. return sprintf('%.E', $value);
  984. }
  985. }
  986. }
  987. /**
  988. * Formats the value as a currency number.
  989. *
  990. * This function does not require the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed
  991. * to work, but it is highly recommended to install it to get good formatting results.
  992. *
  993. * @param mixed $value the value to be formatted.
  994. * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
  995. * If null, [[currencyCode]] will be used.
  996. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  997. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  998. * @return string the formatted result.
  999. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1000. * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
  1001. */
  1002. public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
  1003. {
  1004. if ($value === null) {
  1005. return $this->nullDisplay;
  1006. }
  1007. $value = $this->normalizeNumericValue($value);
  1008. if ($this->_intlLoaded) {
  1009. $currency = $currency ?: $this->currencyCode;
  1010. // currency code must be set before fraction digits
  1011. // http://php.net/manual/en/numberformatter.formatcurrency.php#114376
  1012. if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
  1013. $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
  1014. }
  1015. $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
  1016. if ($currency === null) {
  1017. $result = $formatter->format($value);
  1018. } else {
  1019. $result = $formatter->formatCurrency($value, $currency);
  1020. }
  1021. if ($result === false) {
  1022. throw new InvalidParamException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
  1023. }
  1024. return $result;
  1025. } else {
  1026. if ($currency === null) {
  1027. if ($this->currencyCode === null) {
  1028. 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.');
  1029. }
  1030. $currency = $this->currencyCode;
  1031. }
  1032. return $currency . ' ' . $this->asDecimal($value, 2, $options, $textOptions);
  1033. }
  1034. }
  1035. /**
  1036. * Formats the value as a number spellout.
  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 asSpellout($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::SPELLOUT);
  1053. if (($result = $f->format($value)) === false) {
  1054. throw new InvalidParamException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1055. }
  1056. return $result;
  1057. } else {
  1058. throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
  1059. }
  1060. }
  1061. /**
  1062. * Formats the value as a ordinal value of a number.
  1063. *
  1064. * This function requires the [PHP intl extension](http://php.net/manual/en/book.intl.php) to be installed.
  1065. *
  1066. * @param mixed $value the value to be formatted
  1067. * @return string the formatted result.
  1068. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1069. * @throws InvalidConfigException when the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available.
  1070. */
  1071. public function asOrdinal($value)
  1072. {
  1073. if ($value === null) {
  1074. return $this->nullDisplay;
  1075. }
  1076. $value = $this->normalizeNumericValue($value);
  1077. if ($this->_intlLoaded) {
  1078. $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
  1079. if (($result = $f->format($value)) === false) {
  1080. throw new InvalidParamException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1081. }
  1082. return $result;
  1083. } else {
  1084. throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
  1085. }
  1086. }
  1087. /**
  1088. * Formats the value in bytes as a size in human readable form for example `12 KB`.
  1089. *
  1090. * This is the short form of [[asSize]].
  1091. *
  1092. * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
  1093. * are used in the formatting result.
  1094. *
  1095. * @param string|int|float $value value in bytes to be formatted.
  1096. * @param int $decimals the number of digits after the decimal point.
  1097. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1098. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1099. * @return string the formatted result.
  1100. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1101. * @see sizeFormatBase
  1102. * @see asSize
  1103. */
  1104. public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
  1105. {
  1106. if ($value === null) {
  1107. return $this->nullDisplay;
  1108. }
  1109. list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
  1110. if ($this->sizeFormatBase == 1024) {
  1111. switch ($position) {
  1112. case 0:
  1113. return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
  1114. case 1:
  1115. return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
  1116. case 2:
  1117. return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
  1118. case 3:
  1119. return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
  1120. case 4:
  1121. return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
  1122. default:
  1123. return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
  1124. }
  1125. } else {
  1126. switch ($position) {
  1127. case 0:
  1128. return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
  1129. case 1:
  1130. return Yii::t('yii', '{nFormatted} KB', $params, $this->locale);
  1131. case 2:
  1132. return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
  1133. case 3:
  1134. return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
  1135. case 4:
  1136. return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
  1137. default:
  1138. return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
  1139. }
  1140. }
  1141. }
  1142. /**
  1143. * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
  1144. *
  1145. * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
  1146. * are used in the formatting result.
  1147. *
  1148. * @param string|int|float $value value in bytes to be formatted.
  1149. * @param int $decimals the number of digits after the decimal point.
  1150. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1151. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1152. * @return string the formatted result.
  1153. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1154. * @see sizeFormatBase
  1155. * @see asShortSize
  1156. */
  1157. public function asSize($value, $decimals = null, $options = [], $textOptions = [])
  1158. {
  1159. if ($value === null) {
  1160. return $this->nullDisplay;
  1161. }
  1162. list($params, $position) = $this->formatSizeNumber($value, $decimals, $options, $textOptions);
  1163. if ($this->sizeFormatBase == 1024) {
  1164. switch ($position) {
  1165. case 0:
  1166. return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
  1167. case 1:
  1168. return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
  1169. case 2:
  1170. return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
  1171. case 3:
  1172. return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
  1173. case 4:
  1174. return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
  1175. default:
  1176. return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
  1177. }
  1178. } else {
  1179. switch ($position) {
  1180. case 0:
  1181. return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
  1182. case 1:
  1183. return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
  1184. case 2:
  1185. return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
  1186. case 3:
  1187. return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
  1188. case 4:
  1189. return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
  1190. default:
  1191. return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
  1192. }
  1193. }
  1194. }
  1195. /**
  1196. * Given the value in bytes formats number part of the human readable form.
  1197. *
  1198. * @param string|int|float $value value in bytes to be formatted.
  1199. * @param int $decimals the number of digits after the decimal point
  1200. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1201. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1202. * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
  1203. * @throws InvalidParamException if the input value is not numeric or the formatting failed.
  1204. */
  1205. private function formatSizeNumber($value, $decimals, $options, $textOptions)
  1206. {
  1207. $value = $this->normalizeNumericValue($value);
  1208. $position = 0;
  1209. do {
  1210. if (abs($value) < $this->sizeFormatBase) {
  1211. break;
  1212. }
  1213. $value /= $this->sizeFormatBase;
  1214. $position++;
  1215. } while ($position < 5);
  1216. // no decimals for bytes
  1217. if ($position === 0) {
  1218. $decimals = 0;
  1219. } elseif ($decimals !== null) {
  1220. $value = round($value, $decimals);
  1221. }
  1222. // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
  1223. $oldThousandSeparator = $this->thousandSeparator;
  1224. $this->thousandSeparator = '';
  1225. if ($this->_intlLoaded) {
  1226. $options[NumberFormatter::GROUPING_USED] = false;
  1227. }
  1228. // format the size value
  1229. $params = [
  1230. // this is the unformatted number used for the plural rule
  1231. // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
  1232. // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
  1233. 'n' => abs($value),
  1234. // this is the formatted number used for display
  1235. 'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
  1236. ];
  1237. $this->thousandSeparator = $oldThousandSeparator;
  1238. return [$params, $position];
  1239. }
  1240. /**
  1241. * Normalizes a numeric input value
  1242. *
  1243. * - everything [empty](http://php.net/manual/en/function.empty.php) will result in `0`
  1244. * - a [numeric](http://php.net/manual/en/function.is-numeric.php) string will be casted to float
  1245. * - everything else will be returned if it is [numeric](http://php.net/manual/en/function.is-numeric.php),
  1246. * otherwise an exception is thrown.
  1247. *
  1248. * @param mixed $value the input value
  1249. * @return float|int the normalized number value
  1250. * @throws InvalidParamException if the input value is not numeric.
  1251. */
  1252. protected function normalizeNumericValue($value)
  1253. {
  1254. if (empty($value)) {
  1255. return 0;
  1256. }
  1257. if (is_string($value) && is_numeric($value)) {
  1258. $value = (float) $value;
  1259. }
  1260. if (!is_numeric($value)) {
  1261. throw new InvalidParamException("'$value' is not a numeric value.");
  1262. }
  1263. return $value;
  1264. }
  1265. /**
  1266. * Creates a number formatter based on the given type and format.
  1267. *
  1268. * You may override this method to create a number formatter based on patterns.
  1269. *
  1270. * @param int $style the type of the number formatter.
  1271. * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
  1272. * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
  1273. * @param int $decimals the number of digits after the decimal point.
  1274. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1275. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1276. * @return NumberFormatter the created formatter instance
  1277. */
  1278. protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
  1279. {
  1280. $formatter = new NumberFormatter($this->locale, $style);
  1281. // set text attributes
  1282. foreach ($this->numberFormatterTextOptions as $name => $attribute) {
  1283. $formatter->setTextAttribute($name, $attribute);
  1284. }
  1285. foreach ($textOptions as $name => $attribute) {
  1286. $formatter->setTextAttribute($name, $attribute);
  1287. }
  1288. // set attributes
  1289. foreach ($this->numberFormatterOptions as $name => $value) {
  1290. $formatter->setAttribute($name, $value);
  1291. }
  1292. foreach ($options as $name => $value) {
  1293. $formatter->setAttribute($name, $value);
  1294. }
  1295. if ($decimals !== null) {
  1296. $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
  1297. $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
  1298. }
  1299. // set symbols
  1300. if ($this->decimalSeparator !== null) {
  1301. $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
  1302. }
  1303. if ($this->thousandSeparator !== null) {
  1304. $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
  1305. $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
  1306. }
  1307. foreach ($this->numberFormatterSymbols as $name => $symbol) {
  1308. $formatter->setSymbol($name, $symbol);
  1309. }
  1310. return $formatter;
  1311. }
  1312. }