Formatter.php 63 KB

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