Formatter.php 91 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063
  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 Closure;
  9. use DateInterval;
  10. use DateTime;
  11. use DateTimeInterface;
  12. use DateTimeZone;
  13. use IntlDateFormatter;
  14. use NumberFormatter;
  15. use Yii;
  16. use yii\base\Component;
  17. use yii\base\InvalidArgumentException;
  18. use yii\base\InvalidConfigException;
  19. use yii\helpers\FormatConverter;
  20. use yii\helpers\Html;
  21. use yii\helpers\HtmlPurifier;
  22. /**
  23. * Formatter provides a set of commonly used data formatting methods.
  24. *
  25. * The formatting methods provided by Formatter are all named in the form of `asXyz()`.
  26. * The behavior of some of them may be configured via the properties of Formatter. For example,
  27. * by configuring [[dateFormat]], one may control how [[asDate()]] formats the value into a date string.
  28. *
  29. * Formatter is configured as an application component in [[\yii\base\Application]] by default.
  30. * You can access that instance via `Yii::$app->formatter`.
  31. *
  32. * The Formatter class is designed to format values according to a [[locale]]. For this feature to work
  33. * the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) has to be installed.
  34. * Most of the methods however work also if the PHP intl extension is not installed by providing
  35. * a fallback implementation. Without intl month and day names are in English only.
  36. * Note that even if the intl extension is installed, formatting date and time values for years >=2038 or <=1901
  37. * on 32bit systems will fall back to the PHP implementation because intl uses a 32bit UNIX timestamp internally.
  38. * On a 64bit system the intl formatter is used in all cases if installed.
  39. *
  40. * > Note: The Formatter class is meant to be used for formatting values for display to users in different
  41. * > languages and time zones. If you need to format a date or time in machine readable format, use the
  42. * > PHP [date()](https://secure.php.net/manual/en/function.date.php) function instead.
  43. *
  44. * @author Qiang Xue <qiang.xue@gmail.com>
  45. * @author Enrica Ruedin <e.ruedin@guggach.com>
  46. * @author Carsten Brandt <mail@cebe.cc>
  47. * @since 2.0
  48. */
  49. class Formatter extends Component
  50. {
  51. /**
  52. * @since 2.0.13
  53. */
  54. const UNIT_SYSTEM_METRIC = 'metric';
  55. /**
  56. * @since 2.0.13
  57. */
  58. const UNIT_SYSTEM_IMPERIAL = 'imperial';
  59. /**
  60. * @since 2.0.13
  61. */
  62. const FORMAT_WIDTH_LONG = 'long';
  63. /**
  64. * @since 2.0.13
  65. */
  66. const FORMAT_WIDTH_SHORT = 'short';
  67. /**
  68. * @since 2.0.13
  69. */
  70. const UNIT_LENGTH = 'length';
  71. /**
  72. * @since 2.0.13
  73. */
  74. const UNIT_WEIGHT = 'mass';
  75. /**
  76. * @var string the text to be displayed when formatting a `null` value.
  77. * Defaults to `'<span class="not-set">(not set)</span>'`, where `(not set)`
  78. * will be translated according to [[locale]].
  79. */
  80. public $nullDisplay;
  81. /**
  82. * @var array the text to be displayed when formatting a boolean value. The first element corresponds
  83. * to the text displayed for `false`, the second element for `true`.
  84. * Defaults to `['No', 'Yes']`, where `Yes` and `No`
  85. * will be translated according to [[locale]].
  86. */
  87. public $booleanFormat;
  88. /**
  89. * @var string the locale ID that is used to localize the date and number formatting.
  90. * For number and date formatting this is only effective when the
  91. * [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is installed.
  92. * If not set, [[\yii\base\Application::language]] will be used.
  93. */
  94. public $locale;
  95. /**
  96. * @var string the time zone to use for formatting time and date values.
  97. *
  98. * This can be any value that may be passed to [date_default_timezone_set()](https://secure.php.net/manual/en/function.date-default-timezone-set.php)
  99. * e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
  100. * Refer to the [php manual](https://secure.php.net/manual/en/timezones.php) for available time zones.
  101. * If this property is not set, [[\yii\base\Application::timeZone]] will be used.
  102. *
  103. * 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.
  104. * If you store your data in a different time zone in the database, you have to adjust [[defaultTimeZone]] accordingly.
  105. */
  106. public $timeZone;
  107. /**
  108. * @var string the time zone that is assumed for input values if they do not include a time zone explicitly.
  109. *
  110. * The value must be a valid time zone identifier, e.g. `UTC`, `Europe/Berlin` or `America/Chicago`.
  111. * Please refer to the [php manual](https://secure.php.net/manual/en/timezones.php) for available time zones.
  112. *
  113. * It defaults to `UTC` so you only have to adjust this value if you store datetime values in another time zone in your database.
  114. *
  115. * Note that a UNIX timestamp is always in UTC by its definition. That means that specifying a default time zone different from
  116. * UTC has no effect on date values given as UNIX timestamp.
  117. *
  118. * @since 2.0.1
  119. */
  120. public $defaultTimeZone = 'UTC';
  121. /**
  122. * @var string the default format string to be used to format a [[asDate()|date]].
  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. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  127. * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
  128. *
  129. * For example:
  130. *
  131. * ```php
  132. * 'MM/dd/yyyy' // date in ICU format
  133. * 'php:m/d/Y' // the same date in PHP format
  134. * ```
  135. */
  136. public $dateFormat = 'medium';
  137. /**
  138. * @var string the default format string to be used to format a [[asTime()|time]].
  139. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  140. *
  141. * 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).
  142. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  143. * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
  144. *
  145. * For example:
  146. *
  147. * ```php
  148. * 'HH:mm:ss' // time in ICU format
  149. * 'php:H:i:s' // the same time in PHP format
  150. * ```
  151. */
  152. public $timeFormat = 'medium';
  153. /**
  154. * @var string the default format string to be used to format a [[asDatetime()|date and time]].
  155. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  156. *
  157. * 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).
  158. *
  159. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  160. * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
  161. *
  162. * For example:
  163. *
  164. * ```php
  165. * 'MM/dd/yyyy HH:mm:ss' // date and time in ICU format
  166. * 'php:m/d/Y H:i:s' // the same date and time in PHP format
  167. * ```
  168. */
  169. public $datetimeFormat = 'medium';
  170. /**
  171. * @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
  172. * passed to the [constructor of the `IntlDateFormatter` class](https://secure.php.net/manual/en/intldateformatter.create.php).
  173. *
  174. * Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
  175. * `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
  176. *
  177. * To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
  178. * set this property to `\IntlDateFormatter::TRADITIONAL`.
  179. * The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
  180. *
  181. * ```php
  182. * 'formatter' => [
  183. * 'locale' => 'fa_IR@calendar=persian',
  184. * 'calendar' => \IntlDateFormatter::TRADITIONAL,
  185. * ],
  186. * ```
  187. *
  188. * Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar).
  189. *
  190. * Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
  191. * Check the [PHP manual](https://secure.php.net/manual/en/intldateformatter.create.php) for more details.
  192. *
  193. * If the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
  194. *
  195. * @see https://secure.php.net/manual/en/intldateformatter.create.php
  196. * @see https://secure.php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
  197. * @see https://secure.php.net/manual/en/class.intlcalendar.php
  198. * @since 2.0.7
  199. */
  200. public $calendar;
  201. /**
  202. * @var string the character displayed as the decimal point when formatting a number.
  203. * If not set, the decimal separator corresponding to [[locale]] will be used.
  204. * If [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, the default value is '.'.
  205. */
  206. public $decimalSeparator;
  207. /**
  208. * @var string the character displayed as the thousands separator (also called grouping separator) character when formatting a number.
  209. * If not set, the thousand separator corresponding to [[locale]] will be used.
  210. * If [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, the default value is ','.
  211. */
  212. public $thousandSeparator;
  213. /**
  214. * @var array a list of name value pairs that are passed to the
  215. * intl [NumberFormatter::setAttribute()](https://secure.php.net/manual/en/numberformatter.setattribute.php) method of all
  216. * the number formatter objects created by [[createNumberFormatter()]].
  217. * This property takes only effect if the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is installed.
  218. *
  219. * Please refer to the [PHP manual](https://secure.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatattribute)
  220. * for the possible options.
  221. *
  222. * For example to adjust the maximum and minimum value of fraction digits you can configure this property like the following:
  223. *
  224. * ```php
  225. * [
  226. * NumberFormatter::MIN_FRACTION_DIGITS => 0,
  227. * NumberFormatter::MAX_FRACTION_DIGITS => 2,
  228. * ]
  229. * ```
  230. */
  231. public $numberFormatterOptions = [];
  232. /**
  233. * @var array a list of name value pairs that are passed to the
  234. * intl [NumberFormatter::setTextAttribute()](https://secure.php.net/manual/en/numberformatter.settextattribute.php) method of all
  235. * the number formatter objects created by [[createNumberFormatter()]].
  236. * This property takes only effect if the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is installed.
  237. *
  238. * Please refer to the [PHP manual](https://secure.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformattextattribute)
  239. * for the possible options.
  240. *
  241. * For example to change the minus sign for negative numbers you can configure this property like the following:
  242. *
  243. * ```php
  244. * [
  245. * NumberFormatter::NEGATIVE_PREFIX => 'MINUS',
  246. * ]
  247. * ```
  248. */
  249. public $numberFormatterTextOptions = [];
  250. /**
  251. * @var array a list of name value pairs that are passed to the
  252. * intl [NumberFormatter::setSymbol()](https://secure.php.net/manual/en/numberformatter.setsymbol.php) method of all
  253. * the number formatter objects created by [[createNumberFormatter()]].
  254. * This property takes only effect if the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is installed.
  255. *
  256. * Please refer to the [PHP manual](https://secure.php.net/manual/en/class.numberformatter.php#intl.numberformatter-constants.unumberformatsymbol)
  257. * for the possible options.
  258. *
  259. * For example to choose a custom currency symbol, e.g. [U+20BD](http://unicode-table.com/en/20BD/) instead of `руб.` for Russian Ruble:
  260. *
  261. * ```php
  262. * [
  263. * NumberFormatter::CURRENCY_SYMBOL => '₽',
  264. * ]
  265. * ```
  266. *
  267. * @since 2.0.4
  268. */
  269. public $numberFormatterSymbols = [];
  270. /**
  271. * @var string the 3-letter ISO 4217 currency code indicating the default currency to use for [[asCurrency]].
  272. * If not set, the currency code corresponding to [[locale]] will be used.
  273. * Note that in this case the [[locale]] has to be specified with a country code, e.g. `en-US` otherwise it
  274. * is not possible to determine the default currency.
  275. */
  276. public $currencyCode;
  277. /**
  278. * @var int the base at which a kilobyte is calculated (1000 or 1024 bytes per kilobyte), used by [[asSize]] and [[asShortSize]].
  279. * Defaults to 1024.
  280. */
  281. public $sizeFormatBase = 1024;
  282. /**
  283. * @var string default system of measure units. Defaults to [[UNIT_SYSTEM_METRIC]].
  284. * Possible values:
  285. * - [[UNIT_SYSTEM_METRIC]]
  286. * - [[UNIT_SYSTEM_IMPERIAL]]
  287. *
  288. * @see asLength
  289. * @see asWeight
  290. * @since 2.0.13
  291. */
  292. public $systemOfUnits = self::UNIT_SYSTEM_METRIC;
  293. /**
  294. * @var array configuration of weight and length measurement units.
  295. * This array contains the most usable measurement units, but you can change it
  296. * in case you have some special requirements.
  297. *
  298. * For example, you can add smaller measure unit:
  299. *
  300. * ```php
  301. * $this->measureUnits[self::UNIT_LENGTH][self::UNIT_SYSTEM_METRIC] = [
  302. * 'nanometer' => 0.000001
  303. * ]
  304. * ```
  305. * @see asLength
  306. * @see asWeight
  307. * @since 2.0.13
  308. */
  309. public $measureUnits = [
  310. self::UNIT_LENGTH => [
  311. self::UNIT_SYSTEM_IMPERIAL => [
  312. 'inch' => 1,
  313. 'foot' => 12,
  314. 'yard' => 36,
  315. 'chain' => 792,
  316. 'furlong' => 7920,
  317. 'mile' => 63360,
  318. ],
  319. self::UNIT_SYSTEM_METRIC => [
  320. 'millimeter' => 1,
  321. 'centimeter' => 10,
  322. 'meter' => 1000,
  323. 'kilometer' => 1000000,
  324. ],
  325. ],
  326. self::UNIT_WEIGHT => [
  327. self::UNIT_SYSTEM_IMPERIAL => [
  328. 'grain' => 1,
  329. 'drachm' => 27.34375,
  330. 'ounce' => 437.5,
  331. 'pound' => 7000,
  332. 'stone' => 98000,
  333. 'quarter' => 196000,
  334. 'hundredweight' => 784000,
  335. 'ton' => 15680000,
  336. ],
  337. self::UNIT_SYSTEM_METRIC => [
  338. 'gram' => 1,
  339. 'kilogram' => 1000,
  340. 'ton' => 1000000,
  341. ],
  342. ],
  343. ];
  344. /**
  345. * @var array The base units that are used as multipliers for smallest possible unit from [[measureUnits]].
  346. * @since 2.0.13
  347. */
  348. public $baseUnits = [
  349. self::UNIT_LENGTH => [
  350. self::UNIT_SYSTEM_IMPERIAL => 12, // 1 feet = 12 inches
  351. self::UNIT_SYSTEM_METRIC => 1000, // 1 meter = 1000 millimeters
  352. ],
  353. self::UNIT_WEIGHT => [
  354. self::UNIT_SYSTEM_IMPERIAL => 7000, // 1 pound = 7000 grains
  355. self::UNIT_SYSTEM_METRIC => 1000, // 1 kilogram = 1000 grams
  356. ],
  357. ];
  358. /**
  359. * @var bool whether the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is loaded.
  360. */
  361. private $_intlLoaded = false;
  362. /**
  363. * @var \ResourceBundle cached ResourceBundle object used to read unit translations
  364. */
  365. private $_resourceBundle;
  366. /**
  367. * @var array cached unit translation patterns
  368. */
  369. private $_unitMessages = [];
  370. /**
  371. * {@inheritdoc}
  372. */
  373. public function init()
  374. {
  375. if ($this->timeZone === null) {
  376. $this->timeZone = Yii::$app->timeZone;
  377. }
  378. if ($this->locale === null) {
  379. $this->locale = Yii::$app->language;
  380. }
  381. if ($this->booleanFormat === null) {
  382. $this->booleanFormat = [Yii::t('yii', 'No', [], $this->locale), Yii::t('yii', 'Yes', [], $this->locale)];
  383. }
  384. if ($this->nullDisplay === null) {
  385. $this->nullDisplay = '<span class="not-set">' . Yii::t('yii', '(not set)', [], $this->locale) . '</span>';
  386. }
  387. $this->_intlLoaded = extension_loaded('intl');
  388. if (!$this->_intlLoaded) {
  389. if ($this->decimalSeparator === null) {
  390. $this->decimalSeparator = '.';
  391. }
  392. if ($this->thousandSeparator === null) {
  393. $this->thousandSeparator = ',';
  394. }
  395. }
  396. }
  397. /**
  398. * Formats the value based on the given format type.
  399. * This method will call one of the "as" methods available in this class to do the formatting.
  400. * For type "xyz", the method "asXyz" will be used. For example, if the format is "html",
  401. * then [[asHtml()]] will be used. Format names are case insensitive.
  402. * @param mixed $value the value to be formatted.
  403. * @param string|array|Closure $format the format of the value, e.g., "html", "text" or an anonymous function
  404. * returning the formatted value.
  405. *
  406. * To specify additional parameters of the formatting method, you may use an array.
  407. * The first element of the array specifies the format name, while the rest of the elements will be used as the
  408. * parameters to the formatting method. For example, a format of `['date', 'Y-m-d']` will cause the invocation
  409. * of `asDate($value, 'Y-m-d')`.
  410. *
  411. * The anonymous function signature should be: `function($value, $formatter)`,
  412. * where `$value` is the value that should be formatted and `$formatter` is an instance of the Formatter class,
  413. * which can be used to call other formatting functions.
  414. * The possibility to use an anonymous function is available since version 2.0.13.
  415. * @return string the formatting result.
  416. * @throws InvalidArgumentException if the format type is not supported by this class.
  417. */
  418. public function format($value, $format)
  419. {
  420. if ($format instanceof Closure) {
  421. return call_user_func($format, $value, $this);
  422. } elseif (is_array($format)) {
  423. if (!isset($format[0])) {
  424. throw new InvalidArgumentException('The $format array must contain at least one element.');
  425. }
  426. $f = $format[0];
  427. $format[0] = $value;
  428. $params = $format;
  429. $format = $f;
  430. } else {
  431. $params = [$value];
  432. }
  433. $method = 'as' . $format;
  434. if ($this->hasMethod($method)) {
  435. return call_user_func_array([$this, $method], $params);
  436. }
  437. throw new InvalidArgumentException("Unknown format type: $format");
  438. }
  439. // simple formats
  440. /**
  441. * Formats the value as is without any formatting.
  442. * This method simply returns back the parameter without any format.
  443. * The only exception is a `null` value which will be formatted using [[nullDisplay]].
  444. * @param mixed $value the value to be formatted.
  445. * @return string the formatted result.
  446. */
  447. public function asRaw($value)
  448. {
  449. if ($value === null) {
  450. return $this->nullDisplay;
  451. }
  452. return $value;
  453. }
  454. /**
  455. * Formats the value as an HTML-encoded plain text.
  456. * @param string $value the value to be formatted.
  457. * @return string the formatted result.
  458. */
  459. public function asText($value)
  460. {
  461. if ($value === null) {
  462. return $this->nullDisplay;
  463. }
  464. return Html::encode($value);
  465. }
  466. /**
  467. * Formats the value as an HTML-encoded plain text with newlines converted into breaks.
  468. * @param string $value the value to be formatted.
  469. * @return string the formatted result.
  470. */
  471. public function asNtext($value)
  472. {
  473. if ($value === null) {
  474. return $this->nullDisplay;
  475. }
  476. return nl2br(Html::encode($value));
  477. }
  478. /**
  479. * Formats the value as HTML-encoded text paragraphs.
  480. * Each text paragraph is enclosed within a `<p>` tag.
  481. * One or multiple consecutive empty lines divide two paragraphs.
  482. * @param string $value the value to be formatted.
  483. * @return string the formatted result.
  484. */
  485. public function asParagraphs($value)
  486. {
  487. if ($value === null) {
  488. return $this->nullDisplay;
  489. }
  490. return str_replace('<p></p>', '', '<p>' . preg_replace('/\R{2,}/u', "</p>\n<p>", Html::encode($value)) . '</p>');
  491. }
  492. /**
  493. * Formats the value as HTML text.
  494. * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks.
  495. * Use [[asRaw()]] if you do not want any purification of the value.
  496. * @param string $value the value to be formatted.
  497. * @param array|null $config the configuration for the HTMLPurifier class.
  498. * @return string the formatted result.
  499. */
  500. public function asHtml($value, $config = null)
  501. {
  502. if ($value === null) {
  503. return $this->nullDisplay;
  504. }
  505. return HtmlPurifier::process($value, $config);
  506. }
  507. /**
  508. * Formats the value as a mailto link.
  509. * @param string $value the value to be formatted.
  510. * @param array $options the tag options in terms of name-value pairs. See [[Html::mailto()]].
  511. * @return string the formatted result.
  512. */
  513. public function asEmail($value, $options = [])
  514. {
  515. if ($value === null) {
  516. return $this->nullDisplay;
  517. }
  518. return Html::mailto(Html::encode($value), $value, $options);
  519. }
  520. /**
  521. * Formats the value as an image tag.
  522. * @param mixed $value the value to be formatted.
  523. * @param array $options the tag options in terms of name-value pairs. See [[Html::img()]].
  524. * @return string the formatted result.
  525. */
  526. public function asImage($value, $options = [])
  527. {
  528. if ($value === null) {
  529. return $this->nullDisplay;
  530. }
  531. return Html::img($value, $options);
  532. }
  533. /**
  534. * Formats the value as a hyperlink.
  535. * @param mixed $value the value to be formatted.
  536. * @param array $options the tag options in terms of name-value pairs. See [[Html::a()]].
  537. * @return string the formatted result.
  538. */
  539. public function asUrl($value, $options = [])
  540. {
  541. if ($value === null) {
  542. return $this->nullDisplay;
  543. }
  544. $url = $value;
  545. if (strpos($url, '://') === false) {
  546. $url = 'http://' . $url;
  547. }
  548. return Html::a(Html::encode($value), $url, $options);
  549. }
  550. /**
  551. * Formats the value as a boolean.
  552. * @param mixed $value the value to be formatted.
  553. * @return string the formatted result.
  554. * @see booleanFormat
  555. */
  556. public function asBoolean($value)
  557. {
  558. if ($value === null) {
  559. return $this->nullDisplay;
  560. }
  561. return $value ? $this->booleanFormat[1] : $this->booleanFormat[0];
  562. }
  563. // date and time formats
  564. /**
  565. * Formats the value as a date.
  566. * @param int|string|DateTime $value the value to be formatted. The following
  567. * types of value are supported:
  568. *
  569. * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
  570. * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
  571. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  572. * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object. You may set the time zone
  573. * for the DateTime object to specify the source time zone.
  574. *
  575. * The formatter will convert date values according to [[timeZone]] before formatting it.
  576. * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
  577. * Also no conversion will be performed on values that have no time information, e.g. `"2017-06-05"`.
  578. *
  579. * @param string $format the format used to convert the value into a date string.
  580. * If null, [[dateFormat]] will be used.
  581. *
  582. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  583. * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
  584. *
  585. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  586. * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
  587. *
  588. * @return string the formatted result.
  589. * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
  590. * @throws InvalidConfigException if the date format is invalid.
  591. * @see dateFormat
  592. */
  593. public function asDate($value, $format = null)
  594. {
  595. if ($format === null) {
  596. $format = $this->dateFormat;
  597. }
  598. return $this->formatDateTimeValue($value, $format, 'date');
  599. }
  600. /**
  601. * Formats the value as a time.
  602. * @param int|string|DateTime $value the value to be formatted. The following
  603. * types of value are supported:
  604. *
  605. * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
  606. * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
  607. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  608. * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object. You may set the time zone
  609. * for the DateTime object to specify the source time zone.
  610. *
  611. * The formatter will convert date values according to [[timeZone]] before formatting it.
  612. * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
  613. *
  614. * @param string $format the format used to convert the value into a date string.
  615. * If null, [[timeFormat]] will be used.
  616. *
  617. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  618. * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
  619. *
  620. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  621. * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
  622. *
  623. * @return string the formatted result.
  624. * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
  625. * @throws InvalidConfigException if the date format is invalid.
  626. * @see timeFormat
  627. */
  628. public function asTime($value, $format = null)
  629. {
  630. if ($format === null) {
  631. $format = $this->timeFormat;
  632. }
  633. return $this->formatDateTimeValue($value, $format, 'time');
  634. }
  635. /**
  636. * Formats the value as a datetime.
  637. * @param int|string|DateTime $value the value to be formatted. The following
  638. * types of value are supported:
  639. *
  640. * - an integer representing a UNIX timestamp. A UNIX timestamp is always in UTC by its definition.
  641. * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
  642. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  643. * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object. You may set the time zone
  644. * for the DateTime object to specify the source time zone.
  645. *
  646. * The formatter will convert date values according to [[timeZone]] before formatting it.
  647. * If no timezone conversion should be performed, you need to set [[defaultTimeZone]] and [[timeZone]] to the same value.
  648. *
  649. * @param string $format the format used to convert the value into a date string.
  650. * If null, [[datetimeFormat]] will be used.
  651. *
  652. * This can be "short", "medium", "long", or "full", which represents a preset format of different lengths.
  653. * It can also be a custom format as specified in the [ICU manual](http://userguide.icu-project.org/formatparse/datetime).
  654. *
  655. * Alternatively this can be a string prefixed with `php:` representing a format that can be recognized by the
  656. * PHP [date()](https://secure.php.net/manual/en/function.date.php)-function.
  657. *
  658. * @return string the formatted result.
  659. * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
  660. * @throws InvalidConfigException if the date format is invalid.
  661. * @see datetimeFormat
  662. */
  663. public function asDatetime($value, $format = null)
  664. {
  665. if ($format === null) {
  666. $format = $this->datetimeFormat;
  667. }
  668. return $this->formatDateTimeValue($value, $format, 'datetime');
  669. }
  670. /**
  671. * @var array map of short format names to IntlDateFormatter constant values.
  672. */
  673. private $_dateFormats = [
  674. 'short' => 3, // IntlDateFormatter::SHORT,
  675. 'medium' => 2, // IntlDateFormatter::MEDIUM,
  676. 'long' => 1, // IntlDateFormatter::LONG,
  677. 'full' => 0, // IntlDateFormatter::FULL,
  678. ];
  679. /**
  680. * @param int|string|DateTime $value the value to be formatted. The following
  681. * types of value are supported:
  682. *
  683. * - an integer representing a UNIX timestamp
  684. * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
  685. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  686. * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
  687. *
  688. * @param string $format the format used to convert the value into a date string.
  689. * @param string $type 'date', 'time', or 'datetime'.
  690. * @throws InvalidConfigException if the date format is invalid.
  691. * @return string the formatted result.
  692. */
  693. private function formatDateTimeValue($value, $format, $type)
  694. {
  695. $timeZone = $this->timeZone;
  696. // avoid time zone conversion for date-only and time-only values
  697. if ($type === 'date' || $type === 'time') {
  698. list($timestamp, $hasTimeInfo, $hasDateInfo) = $this->normalizeDatetimeValue($value, true);
  699. if ($type === 'date' && !$hasTimeInfo || $type === 'time' && !$hasDateInfo) {
  700. $timeZone = $this->defaultTimeZone;
  701. }
  702. } else {
  703. $timestamp = $this->normalizeDatetimeValue($value);
  704. }
  705. if ($timestamp === null) {
  706. return $this->nullDisplay;
  707. }
  708. // intl does not work with dates >=2038 or <=1901 on 32bit machines, fall back to PHP
  709. $year = $timestamp->format('Y');
  710. if ($this->_intlLoaded && !(PHP_INT_SIZE === 4 && ($year <= 1901 || $year >= 2038))) {
  711. if (strncmp($format, 'php:', 4) === 0) {
  712. $format = FormatConverter::convertDatePhpToIcu(substr($format, 4));
  713. }
  714. if (isset($this->_dateFormats[$format])) {
  715. if ($type === 'date') {
  716. $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar);
  717. } elseif ($type === 'time') {
  718. $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar);
  719. } else {
  720. $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar);
  721. }
  722. } else {
  723. $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
  724. }
  725. if ($formatter === null) {
  726. throw new InvalidConfigException(intl_get_error_message());
  727. }
  728. // make IntlDateFormatter work with DateTimeImmutable
  729. if ($timestamp instanceof \DateTimeImmutable) {
  730. $timestamp = new DateTime($timestamp->format(DateTime::ISO8601), $timestamp->getTimezone());
  731. }
  732. return $formatter->format($timestamp);
  733. }
  734. if (strncmp($format, 'php:', 4) === 0) {
  735. $format = substr($format, 4);
  736. } else {
  737. $format = FormatConverter::convertDateIcuToPhp($format, $type, $this->locale);
  738. }
  739. if ($timeZone != null) {
  740. if ($timestamp instanceof \DateTimeImmutable) {
  741. $timestamp = $timestamp->setTimezone(new DateTimeZone($timeZone));
  742. } else {
  743. $timestamp->setTimezone(new DateTimeZone($timeZone));
  744. }
  745. }
  746. return $timestamp->format($format);
  747. }
  748. /**
  749. * Normalizes the given datetime value as a DateTime object that can be taken by various date/time formatting methods.
  750. *
  751. * @param int|string|DateTime $value the datetime value to be normalized. The following
  752. * types of value are supported:
  753. *
  754. * - an integer representing a UNIX timestamp
  755. * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
  756. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  757. * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
  758. *
  759. * @param bool $checkDateTimeInfo whether to also check if the date/time value has some time and date information attached.
  760. * Defaults to `false`. If `true`, the method will then return an array with the first element being the normalized
  761. * timestamp, the second a boolean indicating whether the timestamp has time information and third a boolean indicating
  762. * whether the timestamp has date information.
  763. * This parameter is available since version 2.0.1.
  764. * @return DateTime|array the normalized datetime value.
  765. * Since version 2.0.1 this may also return an array if `$checkDateTimeInfo` is true.
  766. * The first element of the array is the normalized timestamp and the second is a boolean indicating whether
  767. * the timestamp has time information or it is just a date value.
  768. * Since version 2.0.12 the array has third boolean element indicating whether the timestamp has date information
  769. * or it is just a time value.
  770. * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
  771. */
  772. protected function normalizeDatetimeValue($value, $checkDateTimeInfo = false)
  773. {
  774. // checking for DateTime and DateTimeInterface is not redundant, DateTimeInterface is only in PHP>5.5
  775. if ($value === null || $value instanceof DateTime || $value instanceof DateTimeInterface) {
  776. // skip any processing
  777. return $checkDateTimeInfo ? [$value, true, true] : $value;
  778. }
  779. if (empty($value)) {
  780. $value = 0;
  781. }
  782. try {
  783. if (is_numeric($value)) { // process as unix timestamp, which is always in UTC
  784. $timestamp = new DateTime('@' . (int) $value, new DateTimeZone('UTC'));
  785. return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
  786. } 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)
  787. return $checkDateTimeInfo ? [$timestamp, false, true] : $timestamp;
  788. } 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)
  789. return $checkDateTimeInfo ? [$timestamp, true, true] : $timestamp;
  790. }
  791. // finally try to create a DateTime object with the value
  792. if ($checkDateTimeInfo) {
  793. $timestamp = new DateTime($value, new DateTimeZone($this->defaultTimeZone));
  794. $info = date_parse($value);
  795. return [
  796. $timestamp,
  797. !($info['hour'] === false && $info['minute'] === false && $info['second'] === false),
  798. !($info['year'] === false && $info['month'] === false && $info['day'] === false && empty($info['zone'])),
  799. ];
  800. }
  801. return new DateTime($value, new DateTimeZone($this->defaultTimeZone));
  802. } catch (\Exception $e) {
  803. throw new InvalidArgumentException("'$value' is not a valid date time value: " . $e->getMessage()
  804. . "\n" . print_r(DateTime::getLastErrors(), true), $e->getCode(), $e);
  805. }
  806. }
  807. /**
  808. * Formats a date, time or datetime in a float number as UNIX timestamp (seconds since 01-01-1970).
  809. * @param int|string|DateTime $value the value to be formatted. The following
  810. * types of value are supported:
  811. *
  812. * - an integer representing a UNIX timestamp
  813. * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
  814. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  815. * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
  816. *
  817. * @return string the formatted result.
  818. */
  819. public function asTimestamp($value)
  820. {
  821. if ($value === null) {
  822. return $this->nullDisplay;
  823. }
  824. $timestamp = $this->normalizeDatetimeValue($value);
  825. return number_format($timestamp->format('U'), 0, '.', '');
  826. }
  827. /**
  828. * Formats the value as the time interval between a date and now in human readable form.
  829. *
  830. * This method can be used in three different ways:
  831. *
  832. * 1. Using a timestamp that is relative to `now`.
  833. * 2. Using a timestamp that is relative to the `$referenceTime`.
  834. * 3. Using a `DateInterval` object.
  835. *
  836. * @param int|string|DateTime|DateInterval $value the value to be formatted. The following
  837. * types of value are supported:
  838. *
  839. * - an integer representing a UNIX timestamp
  840. * - a string that can be [parsed to create a DateTime object](https://secure.php.net/manual/en/datetime.formats.php).
  841. * The timestamp is assumed to be in [[defaultTimeZone]] unless a time zone is explicitly given.
  842. * - a PHP [DateTime](https://secure.php.net/manual/en/class.datetime.php) object
  843. * - a PHP DateInterval object (a positive time interval will refer to the past, a negative one to the future)
  844. *
  845. * @param int|string|DateTime $referenceTime if specified the value is used as a reference time instead of `now`
  846. * when `$value` is not a `DateInterval` object.
  847. * @return string the formatted result.
  848. * @throws InvalidArgumentException if the input value can not be evaluated as a date value.
  849. */
  850. public function asRelativeTime($value, $referenceTime = null)
  851. {
  852. if ($value === null) {
  853. return $this->nullDisplay;
  854. }
  855. if ($value instanceof DateInterval) {
  856. $interval = $value;
  857. } else {
  858. $timestamp = $this->normalizeDatetimeValue($value);
  859. if ($timestamp === false) {
  860. // $value is not a valid date/time value, so we try
  861. // to create a DateInterval with it
  862. try {
  863. $interval = new DateInterval($value);
  864. } catch (\Exception $e) {
  865. // invalid date/time and invalid interval
  866. return $this->nullDisplay;
  867. }
  868. } else {
  869. $timeZone = new DateTimeZone($this->timeZone);
  870. if ($referenceTime === null) {
  871. $dateNow = new DateTime('now', $timeZone);
  872. } else {
  873. $dateNow = $this->normalizeDatetimeValue($referenceTime);
  874. $dateNow->setTimezone($timeZone);
  875. }
  876. $dateThen = $timestamp->setTimezone($timeZone);
  877. $interval = $dateThen->diff($dateNow);
  878. }
  879. }
  880. if ($interval->invert) {
  881. if ($interval->y >= 1) {
  882. return Yii::t('yii', 'in {delta, plural, =1{a year} other{# years}}', ['delta' => $interval->y], $this->locale);
  883. }
  884. if ($interval->m >= 1) {
  885. return Yii::t('yii', 'in {delta, plural, =1{a month} other{# months}}', ['delta' => $interval->m], $this->locale);
  886. }
  887. if ($interval->d >= 1) {
  888. return Yii::t('yii', 'in {delta, plural, =1{a day} other{# days}}', ['delta' => $interval->d], $this->locale);
  889. }
  890. if ($interval->h >= 1) {
  891. return Yii::t('yii', 'in {delta, plural, =1{an hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
  892. }
  893. if ($interval->i >= 1) {
  894. return Yii::t('yii', 'in {delta, plural, =1{a minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
  895. }
  896. if ($interval->s == 0) {
  897. return Yii::t('yii', 'just now', [], $this->locale);
  898. }
  899. return Yii::t('yii', 'in {delta, plural, =1{a second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  900. }
  901. if ($interval->y >= 1) {
  902. return Yii::t('yii', '{delta, plural, =1{a year} other{# years}} ago', ['delta' => $interval->y], $this->locale);
  903. }
  904. if ($interval->m >= 1) {
  905. return Yii::t('yii', '{delta, plural, =1{a month} other{# months}} ago', ['delta' => $interval->m], $this->locale);
  906. }
  907. if ($interval->d >= 1) {
  908. return Yii::t('yii', '{delta, plural, =1{a day} other{# days}} ago', ['delta' => $interval->d], $this->locale);
  909. }
  910. if ($interval->h >= 1) {
  911. return Yii::t('yii', '{delta, plural, =1{an hour} other{# hours}} ago', ['delta' => $interval->h], $this->locale);
  912. }
  913. if ($interval->i >= 1) {
  914. return Yii::t('yii', '{delta, plural, =1{a minute} other{# minutes}} ago', ['delta' => $interval->i], $this->locale);
  915. }
  916. if ($interval->s == 0) {
  917. return Yii::t('yii', 'just now', [], $this->locale);
  918. }
  919. return Yii::t('yii', '{delta, plural, =1{a second} other{# seconds}} ago', ['delta' => $interval->s], $this->locale);
  920. }
  921. /**
  922. * Represents the value as duration in human readable format.
  923. *
  924. * @param DateInterval|string|int $value the value to be formatted. Acceptable formats:
  925. * - [DateInterval object](https://secure.php.net/manual/ru/class.dateinterval.php)
  926. * - integer - number of seconds. For example: value `131` represents `2 minutes, 11 seconds`
  927. * - ISO8601 duration format. For example, all of these values represent `1 day, 2 hours, 30 minutes` duration:
  928. * `2015-01-01T13:00:00Z/2015-01-02T13:30:00Z` - between two datetime values
  929. * `2015-01-01T13:00:00Z/P1D2H30M` - time interval after datetime value
  930. * `P1D2H30M/2015-01-02T13:30:00Z` - time interval before datetime value
  931. * `P1D2H30M` - simply a date interval
  932. * `P-1D2H30M` - a negative date interval (`-1 day, 2 hours, 30 minutes`)
  933. *
  934. * @param string $implodeString will be used to concatenate duration parts. Defaults to `, `.
  935. * @param string $negativeSign will be prefixed to the formatted duration, when it is negative. Defaults to `-`.
  936. * @return string the formatted duration.
  937. * @since 2.0.7
  938. */
  939. public function asDuration($value, $implodeString = ', ', $negativeSign = '-')
  940. {
  941. if ($value === null) {
  942. return $this->nullDisplay;
  943. }
  944. if ($value instanceof DateInterval) {
  945. $isNegative = $value->invert;
  946. $interval = $value;
  947. } elseif (is_numeric($value)) {
  948. $isNegative = $value < 0;
  949. $zeroDateTime = (new DateTime())->setTimestamp(0);
  950. $valueDateTime = (new DateTime())->setTimestamp(abs($value));
  951. $interval = $valueDateTime->diff($zeroDateTime);
  952. } elseif (strncmp($value, 'P-', 2) === 0) {
  953. $interval = new DateInterval('P' . substr($value, 2));
  954. $isNegative = true;
  955. } else {
  956. $interval = new DateInterval($value);
  957. $isNegative = $interval->invert;
  958. }
  959. $parts = [];
  960. if ($interval->y > 0) {
  961. $parts[] = Yii::t('yii', '{delta, plural, =1{1 year} other{# years}}', ['delta' => $interval->y], $this->locale);
  962. }
  963. if ($interval->m > 0) {
  964. $parts[] = Yii::t('yii', '{delta, plural, =1{1 month} other{# months}}', ['delta' => $interval->m], $this->locale);
  965. }
  966. if ($interval->d > 0) {
  967. $parts[] = Yii::t('yii', '{delta, plural, =1{1 day} other{# days}}', ['delta' => $interval->d], $this->locale);
  968. }
  969. if ($interval->h > 0) {
  970. $parts[] = Yii::t('yii', '{delta, plural, =1{1 hour} other{# hours}}', ['delta' => $interval->h], $this->locale);
  971. }
  972. if ($interval->i > 0) {
  973. $parts[] = Yii::t('yii', '{delta, plural, =1{1 minute} other{# minutes}}', ['delta' => $interval->i], $this->locale);
  974. }
  975. if ($interval->s > 0) {
  976. $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  977. }
  978. if ($interval->s === 0 && empty($parts)) {
  979. $parts[] = Yii::t('yii', '{delta, plural, =1{1 second} other{# seconds}}', ['delta' => $interval->s], $this->locale);
  980. $isNegative = false;
  981. }
  982. return empty($parts) ? $this->nullDisplay : (($isNegative ? $negativeSign : '') . implode($implodeString, $parts));
  983. }
  984. // number formats
  985. /**
  986. * Formats the value as an integer number by removing any decimal digits without rounding.
  987. *
  988. * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
  989. * without [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) support. For very big numbers it's
  990. * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
  991. *
  992. * @param mixed $value the value to be formatted.
  993. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  994. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  995. * @return string the formatted result.
  996. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  997. */
  998. public function asInteger($value, $options = [], $textOptions = [])
  999. {
  1000. if ($value === null) {
  1001. return $this->nullDisplay;
  1002. }
  1003. $normalizedValue = $this->normalizeNumericValue($value);
  1004. if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
  1005. return $this->asIntegerStringFallback((string) $value);
  1006. }
  1007. if ($this->_intlLoaded) {
  1008. $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, null, $options, $textOptions);
  1009. $f->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
  1010. if (($result = $f->format($normalizedValue, NumberFormatter::TYPE_INT64)) === false) {
  1011. throw new InvalidArgumentException('Formatting integer value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1012. }
  1013. return $result;
  1014. }
  1015. return number_format((int) $normalizedValue, 0, $this->decimalSeparator, $this->thousandSeparator);
  1016. }
  1017. /**
  1018. * Formats the value as a decimal number.
  1019. *
  1020. * Property [[decimalSeparator]] will be used to represent the decimal point. The
  1021. * value is rounded automatically to the defined decimal digits.
  1022. *
  1023. * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
  1024. * without [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) support. For very big numbers it's
  1025. * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
  1026. *
  1027. * @param mixed $value the value to be formatted.
  1028. * @param int $decimals the number of digits after the decimal point.
  1029. * If not given, the number of digits depends in the input value and is determined based on
  1030. * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
  1031. * using [[$numberFormatterOptions]].
  1032. * If the PHP intl extension is not available, the default value is `2`.
  1033. * If you want consistent behavior between environments where intl is available and not, you should explicitly
  1034. * specify a value here.
  1035. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1036. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1037. * @return string the formatted result.
  1038. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1039. * @see decimalSeparator
  1040. * @see thousandSeparator
  1041. */
  1042. public function asDecimal($value, $decimals = null, $options = [], $textOptions = [])
  1043. {
  1044. if ($value === null) {
  1045. return $this->nullDisplay;
  1046. }
  1047. $normalizedValue = $this->normalizeNumericValue($value);
  1048. if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
  1049. return $this->asDecimalStringFallback((string) $value, $decimals);
  1050. }
  1051. if ($this->_intlLoaded) {
  1052. $f = $this->createNumberFormatter(NumberFormatter::DECIMAL, $decimals, $options, $textOptions);
  1053. if (($result = $f->format($normalizedValue)) === false) {
  1054. throw new InvalidArgumentException('Formatting decimal value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1055. }
  1056. return $result;
  1057. }
  1058. if ($decimals === null) {
  1059. $decimals = 2;
  1060. }
  1061. return number_format($normalizedValue, $decimals, $this->decimalSeparator, $this->thousandSeparator);
  1062. }
  1063. /**
  1064. * Formats the value as a percent number with "%" sign.
  1065. *
  1066. * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
  1067. * without [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) support. For very big numbers it's
  1068. * recommended to pass them as strings and not use scientific notation otherwise the output might be wrong.
  1069. *
  1070. * @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
  1071. * @param int $decimals the number of digits after the decimal point.
  1072. * If not given, the number of digits depends in the input value and is determined based on
  1073. * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
  1074. * using [[$numberFormatterOptions]].
  1075. * If the PHP intl extension is not available, the default value is `0`.
  1076. * If you want consistent behavior between environments where intl is available and not, you should explicitly
  1077. * specify a value here.
  1078. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1079. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1080. * @return string the formatted result.
  1081. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1082. */
  1083. public function asPercent($value, $decimals = null, $options = [], $textOptions = [])
  1084. {
  1085. if ($value === null) {
  1086. return $this->nullDisplay;
  1087. }
  1088. $normalizedValue = $this->normalizeNumericValue($value);
  1089. if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
  1090. return $this->asPercentStringFallback((string) $value, $decimals);
  1091. }
  1092. if ($this->_intlLoaded) {
  1093. $f = $this->createNumberFormatter(NumberFormatter::PERCENT, $decimals, $options, $textOptions);
  1094. if (($result = $f->format($normalizedValue)) === false) {
  1095. throw new InvalidArgumentException('Formatting percent value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1096. }
  1097. return $result;
  1098. }
  1099. if ($decimals === null) {
  1100. $decimals = 0;
  1101. }
  1102. $normalizedValue *= 100;
  1103. return number_format($normalizedValue, $decimals, $this->decimalSeparator, $this->thousandSeparator) . '%';
  1104. }
  1105. /**
  1106. * Formats the value as a scientific number.
  1107. *
  1108. * @param mixed $value the value to be formatted.
  1109. * @param int $decimals the number of digits after the decimal point.
  1110. * If not given, the number of digits depends in the input value and is determined based on
  1111. * `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
  1112. * using [[$numberFormatterOptions]].
  1113. * If the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available, the default value depends on your PHP configuration.
  1114. * If you want consistent behavior between environments where intl is available and not, you should explicitly
  1115. * specify a value here.
  1116. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1117. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1118. * @return string the formatted result.
  1119. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1120. */
  1121. public function asScientific($value, $decimals = null, $options = [], $textOptions = [])
  1122. {
  1123. if ($value === null) {
  1124. return $this->nullDisplay;
  1125. }
  1126. $value = $this->normalizeNumericValue($value);
  1127. if ($this->_intlLoaded) {
  1128. $f = $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $decimals, $options, $textOptions);
  1129. if (($result = $f->format($value)) === false) {
  1130. throw new InvalidArgumentException('Formatting scientific number value failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1131. }
  1132. return $result;
  1133. }
  1134. if ($decimals !== null) {
  1135. return sprintf("%.{$decimals}E", $value);
  1136. }
  1137. return sprintf('%.E', $value);
  1138. }
  1139. /**
  1140. * Formats the value as a currency number.
  1141. *
  1142. * This function does not require the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) to be installed
  1143. * to work, but it is highly recommended to install it to get good formatting results.
  1144. *
  1145. * Since 2.0.16 numbers that are mispresented after normalization are formatted as strings using fallback function
  1146. * without PHP intl extension support. For very big numbers it's recommended to pass them as strings and not use
  1147. * scientific notation otherwise the output might be wrong.
  1148. *
  1149. * @param mixed $value the value to be formatted.
  1150. * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
  1151. * If null, [[currencyCode]] will be used.
  1152. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1153. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1154. * @return string the formatted result.
  1155. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1156. * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
  1157. */
  1158. public function asCurrency($value, $currency = null, $options = [], $textOptions = [])
  1159. {
  1160. if ($value === null) {
  1161. return $this->nullDisplay;
  1162. }
  1163. $normalizedValue = $this->normalizeNumericValue($value);
  1164. if ($this->isNormalizedValueMispresented($value, $normalizedValue)) {
  1165. return $this->asCurrencyStringFallback((string) $value, $currency);
  1166. }
  1167. if ($this->_intlLoaded) {
  1168. $currency = $currency ?: $this->currencyCode;
  1169. // currency code must be set before fraction digits
  1170. // https://secure.php.net/manual/en/numberformatter.formatcurrency.php#114376
  1171. if ($currency && !isset($textOptions[NumberFormatter::CURRENCY_CODE])) {
  1172. $textOptions[NumberFormatter::CURRENCY_CODE] = $currency;
  1173. }
  1174. $formatter = $this->createNumberFormatter(NumberFormatter::CURRENCY, null, $options, $textOptions);
  1175. if ($currency === null) {
  1176. $result = $formatter->format($normalizedValue);
  1177. } else {
  1178. $result = $formatter->formatCurrency($normalizedValue, $currency);
  1179. }
  1180. if ($result === false) {
  1181. throw new InvalidArgumentException('Formatting currency value failed: ' . $formatter->getErrorCode() . ' ' . $formatter->getErrorMessage());
  1182. }
  1183. return $result;
  1184. }
  1185. if ($currency === null) {
  1186. if ($this->currencyCode === null) {
  1187. 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.');
  1188. }
  1189. $currency = $this->currencyCode;
  1190. }
  1191. return $currency . ' ' . $this->asDecimal($normalizedValue, 2, $options, $textOptions);
  1192. }
  1193. /**
  1194. * Formats the value as a number spellout.
  1195. *
  1196. * This function requires the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) to be installed.
  1197. *
  1198. * This formatter does not work well with very big numbers.
  1199. *
  1200. * @param mixed $value the value to be formatted
  1201. * @return string the formatted result.
  1202. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1203. * @throws InvalidConfigException when the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available.
  1204. */
  1205. public function asSpellout($value)
  1206. {
  1207. if ($value === null) {
  1208. return $this->nullDisplay;
  1209. }
  1210. $value = $this->normalizeNumericValue($value);
  1211. if ($this->_intlLoaded) {
  1212. $f = $this->createNumberFormatter(NumberFormatter::SPELLOUT);
  1213. if (($result = $f->format($value)) === false) {
  1214. throw new InvalidArgumentException('Formatting number as spellout failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1215. }
  1216. return $result;
  1217. }
  1218. throw new InvalidConfigException('Format as Spellout is only supported when PHP intl extension is installed.');
  1219. }
  1220. /**
  1221. * Formats the value as a ordinal value of a number.
  1222. *
  1223. * This function requires the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) to be installed.
  1224. *
  1225. * This formatter does not work well with very big numbers.
  1226. *
  1227. * @param mixed $value the value to be formatted
  1228. * @return string the formatted result.
  1229. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1230. * @throws InvalidConfigException when the [PHP intl extension](https://secure.php.net/manual/en/book.intl.php) is not available.
  1231. */
  1232. public function asOrdinal($value)
  1233. {
  1234. if ($value === null) {
  1235. return $this->nullDisplay;
  1236. }
  1237. $value = $this->normalizeNumericValue($value);
  1238. if ($this->_intlLoaded) {
  1239. $f = $this->createNumberFormatter(NumberFormatter::ORDINAL);
  1240. if (($result = $f->format($value)) === false) {
  1241. throw new InvalidArgumentException('Formatting number as ordinal failed: ' . $f->getErrorCode() . ' ' . $f->getErrorMessage());
  1242. }
  1243. return $result;
  1244. }
  1245. throw new InvalidConfigException('Format as Ordinal is only supported when PHP intl extension is installed.');
  1246. }
  1247. /**
  1248. * Formats the value in bytes as a size in human readable form for example `12 kB`.
  1249. *
  1250. * This is the short form of [[asSize]].
  1251. *
  1252. * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
  1253. * are used in the formatting result.
  1254. *
  1255. * @param string|int|float $value value in bytes to be formatted.
  1256. * @param int $decimals the number of digits after the decimal point.
  1257. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1258. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1259. * @return string the formatted result.
  1260. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1261. * @see sizeFormatBase
  1262. * @see asSize
  1263. */
  1264. public function asShortSize($value, $decimals = null, $options = [], $textOptions = [])
  1265. {
  1266. if ($value === null) {
  1267. return $this->nullDisplay;
  1268. }
  1269. list($params, $position) = $this->formatNumber($value, $decimals, 4, $this->sizeFormatBase, $options, $textOptions);
  1270. if ($this->sizeFormatBase == 1024) {
  1271. switch ($position) {
  1272. case 0:
  1273. return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
  1274. case 1:
  1275. return Yii::t('yii', '{nFormatted} KiB', $params, $this->locale);
  1276. case 2:
  1277. return Yii::t('yii', '{nFormatted} MiB', $params, $this->locale);
  1278. case 3:
  1279. return Yii::t('yii', '{nFormatted} GiB', $params, $this->locale);
  1280. case 4:
  1281. return Yii::t('yii', '{nFormatted} TiB', $params, $this->locale);
  1282. default:
  1283. return Yii::t('yii', '{nFormatted} PiB', $params, $this->locale);
  1284. }
  1285. } else {
  1286. switch ($position) {
  1287. case 0:
  1288. return Yii::t('yii', '{nFormatted} B', $params, $this->locale);
  1289. case 1:
  1290. return Yii::t('yii', '{nFormatted} kB', $params, $this->locale);
  1291. case 2:
  1292. return Yii::t('yii', '{nFormatted} MB', $params, $this->locale);
  1293. case 3:
  1294. return Yii::t('yii', '{nFormatted} GB', $params, $this->locale);
  1295. case 4:
  1296. return Yii::t('yii', '{nFormatted} TB', $params, $this->locale);
  1297. default:
  1298. return Yii::t('yii', '{nFormatted} PB', $params, $this->locale);
  1299. }
  1300. }
  1301. }
  1302. /**
  1303. * Formats the value in bytes as a size in human readable form, for example `12 kilobytes`.
  1304. *
  1305. * If [[sizeFormatBase]] is 1024, [binary prefixes](http://en.wikipedia.org/wiki/Binary_prefix) (e.g. kibibyte/KiB, mebibyte/MiB, ...)
  1306. * are used in the formatting result.
  1307. *
  1308. * @param string|int|float $value value in bytes to be formatted.
  1309. * @param int $decimals the number of digits after the decimal point.
  1310. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1311. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1312. * @return string the formatted result.
  1313. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1314. * @see sizeFormatBase
  1315. * @see asShortSize
  1316. */
  1317. public function asSize($value, $decimals = null, $options = [], $textOptions = [])
  1318. {
  1319. if ($value === null) {
  1320. return $this->nullDisplay;
  1321. }
  1322. list($params, $position) = $this->formatNumber($value, $decimals, 4, $this->sizeFormatBase, $options, $textOptions);
  1323. if ($this->sizeFormatBase == 1024) {
  1324. switch ($position) {
  1325. case 0:
  1326. return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
  1327. case 1:
  1328. return Yii::t('yii', '{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}', $params, $this->locale);
  1329. case 2:
  1330. return Yii::t('yii', '{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}', $params, $this->locale);
  1331. case 3:
  1332. return Yii::t('yii', '{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}', $params, $this->locale);
  1333. case 4:
  1334. return Yii::t('yii', '{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}', $params, $this->locale);
  1335. default:
  1336. return Yii::t('yii', '{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}', $params, $this->locale);
  1337. }
  1338. } else {
  1339. switch ($position) {
  1340. case 0:
  1341. return Yii::t('yii', '{nFormatted} {n, plural, =1{byte} other{bytes}}', $params, $this->locale);
  1342. case 1:
  1343. return Yii::t('yii', '{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}', $params, $this->locale);
  1344. case 2:
  1345. return Yii::t('yii', '{nFormatted} {n, plural, =1{megabyte} other{megabytes}}', $params, $this->locale);
  1346. case 3:
  1347. return Yii::t('yii', '{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}', $params, $this->locale);
  1348. case 4:
  1349. return Yii::t('yii', '{nFormatted} {n, plural, =1{terabyte} other{terabytes}}', $params, $this->locale);
  1350. default:
  1351. return Yii::t('yii', '{nFormatted} {n, plural, =1{petabyte} other{petabytes}}', $params, $this->locale);
  1352. }
  1353. }
  1354. }
  1355. /**
  1356. * Formats the value as a length in human readable form for example `12 meters`.
  1357. * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
  1358. * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
  1359. *
  1360. * @param float|int $value value to be formatted.
  1361. * @param int $decimals the number of digits after the decimal point.
  1362. * @param array $numberOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1363. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1364. * @return string the formatted result.
  1365. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1366. * @throws InvalidConfigException when INTL is not installed or does not contain required information.
  1367. * @see asLength
  1368. * @since 2.0.13
  1369. * @author John Was <janek.jan@gmail.com>
  1370. */
  1371. public function asLength($value, $decimals = null, $numberOptions = [], $textOptions = [])
  1372. {
  1373. return $this->formatUnit(self::UNIT_LENGTH, self::FORMAT_WIDTH_LONG, $value, null, null, $decimals, $numberOptions, $textOptions);
  1374. }
  1375. /**
  1376. * Formats the value as a length in human readable form for example `12 m`.
  1377. * This is the short form of [[asLength]].
  1378. *
  1379. * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
  1380. * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
  1381. *
  1382. * @param float|int $value value to be formatted.
  1383. * @param int $decimals the number of digits after the decimal point.
  1384. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1385. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1386. * @return string the formatted result.
  1387. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1388. * @throws InvalidConfigException when INTL is not installed or does not contain required information.
  1389. * @see asLength
  1390. * @since 2.0.13
  1391. * @author John Was <janek.jan@gmail.com>
  1392. */
  1393. public function asShortLength($value, $decimals = null, $options = [], $textOptions = [])
  1394. {
  1395. return $this->formatUnit(self::UNIT_LENGTH, self::FORMAT_WIDTH_SHORT, $value, null, null, $decimals, $options, $textOptions);
  1396. }
  1397. /**
  1398. * Formats the value as a weight in human readable form for example `12 kilograms`.
  1399. * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
  1400. * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
  1401. *
  1402. * @param float|int $value value to be formatted.
  1403. * @param int $decimals the number of digits after the decimal point.
  1404. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1405. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1406. * @return string the formatted result.
  1407. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1408. * @throws InvalidConfigException when INTL is not installed or does not contain required information.
  1409. * @since 2.0.13
  1410. * @author John Was <janek.jan@gmail.com>
  1411. */
  1412. public function asWeight($value, $decimals = null, $options = [], $textOptions = [])
  1413. {
  1414. return $this->formatUnit(self::UNIT_WEIGHT, self::FORMAT_WIDTH_LONG, $value, null, null, $decimals, $options, $textOptions);
  1415. }
  1416. /**
  1417. * Formats the value as a weight in human readable form for example `12 kg`.
  1418. * This is the short form of [[asWeight]].
  1419. *
  1420. * Check properties [[baseUnits]] if you need to change unit of value as the multiplier
  1421. * of the smallest unit and [[systemOfUnits]] to switch between [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]].
  1422. *
  1423. * @param float|int $value value to be formatted.
  1424. * @param int $decimals the number of digits after the decimal point.
  1425. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1426. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1427. * @return string the formatted result.
  1428. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1429. * @throws InvalidConfigException when INTL is not installed or does not contain required information.
  1430. * @since 2.0.13
  1431. * @author John Was <janek.jan@gmail.com>
  1432. */
  1433. public function asShortWeight($value, $decimals = null, $options = [], $textOptions = [])
  1434. {
  1435. return $this->formatUnit(self::UNIT_WEIGHT, self::FORMAT_WIDTH_SHORT, $value, null, null, $decimals, $options, $textOptions);
  1436. }
  1437. /**
  1438. * @param string $unitType one of [[UNIT_WEIGHT]], [[UNIT_LENGTH]]
  1439. * @param string $unitFormat one of [[FORMAT_WIDTH_SHORT]], [[FORMAT_WIDTH_LONG]]
  1440. * @param float|int $value to be formatted
  1441. * @param float $baseUnit unit of value as the multiplier of the smallest unit. When `null`, property [[baseUnits]]
  1442. * will be used to determine base unit using $unitType and $unitSystem.
  1443. * @param string $unitSystem either [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. When `null`, property [[systemOfUnits]] will be used.
  1444. * @param int $decimals the number of digits after the decimal point.
  1445. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1446. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1447. * @return string
  1448. * @throws InvalidConfigException when INTL is not installed or does not contain required information
  1449. */
  1450. private function formatUnit($unitType, $unitFormat, $value, $baseUnit, $unitSystem, $decimals, $options, $textOptions)
  1451. {
  1452. if ($value === null) {
  1453. return $this->nullDisplay;
  1454. }
  1455. if ($unitSystem === null) {
  1456. $unitSystem = $this->systemOfUnits;
  1457. }
  1458. if ($baseUnit === null) {
  1459. $baseUnit = $this->baseUnits[$unitType][$unitSystem];
  1460. }
  1461. $multipliers = array_values($this->measureUnits[$unitType][$unitSystem]);
  1462. list($params, $position) = $this->formatNumber(
  1463. $this->normalizeNumericValue($value) * $baseUnit,
  1464. $decimals,
  1465. null,
  1466. $multipliers,
  1467. $options,
  1468. $textOptions
  1469. );
  1470. $message = $this->getUnitMessage($unitType, $unitFormat, $unitSystem, $position);
  1471. return (new \MessageFormatter($this->locale, $message))->format([
  1472. '0' => $params['nFormatted'],
  1473. 'n' => $params['n'],
  1474. ]);
  1475. }
  1476. /**
  1477. * @param string $unitType one of [[UNIT_WEIGHT]], [[UNIT_LENGTH]]
  1478. * @param string $unitFormat one of [[FORMAT_WIDTH_SHORT]], [[FORMAT_WIDTH_LONG]]
  1479. * @param string $system either [[UNIT_SYSTEM_METRIC]] or [[UNIT_SYSTEM_IMPERIAL]]. When `null`, property [[systemOfUnits]] will be used.
  1480. * @param int $position internal position of size unit
  1481. * @return string
  1482. * @throws InvalidConfigException when INTL is not installed or does not contain required information
  1483. */
  1484. private function getUnitMessage($unitType, $unitFormat, $system, $position)
  1485. {
  1486. if (isset($this->_unitMessages[$unitType][$system][$position])) {
  1487. return $this->_unitMessages[$unitType][$system][$position];
  1488. }
  1489. if (!$this->_intlLoaded) {
  1490. throw new InvalidConfigException('Format of ' . $unitType . ' is only supported when PHP intl extension is installed.');
  1491. }
  1492. if ($this->_resourceBundle === null) {
  1493. try {
  1494. $this->_resourceBundle = new \ResourceBundle($this->locale, 'ICUDATA-unit');
  1495. } catch (\IntlException $e) {
  1496. throw new InvalidConfigException('Current ICU data does not contain information about measure units. Check system requirements.');
  1497. }
  1498. }
  1499. $unitNames = array_keys($this->measureUnits[$unitType][$system]);
  1500. $bundleKey = 'units' . ($unitFormat === self::FORMAT_WIDTH_SHORT ? 'Short' : '');
  1501. $unitBundle = $this->_resourceBundle[$bundleKey][$unitType][$unitNames[$position]];
  1502. if ($unitBundle === null) {
  1503. throw new InvalidConfigException('Current ICU data version does not contain information about unit type "' . $unitType . '" and unit measure "' . $unitNames[$position] . '". Check system requirements.');
  1504. }
  1505. $message = [];
  1506. foreach ($unitBundle as $key => $value) {
  1507. if ($key === 'dnam') {
  1508. continue;
  1509. }
  1510. $message[] = "$key{{$value}}";
  1511. }
  1512. return $this->_unitMessages[$unitType][$system][$position] = '{n, plural, ' . implode(' ', $message) . '}';
  1513. }
  1514. /**
  1515. * Given the value in bytes formats number part of the human readable form.
  1516. *
  1517. * @param string|int|float $value value in bytes to be formatted.
  1518. * @param int $decimals the number of digits after the decimal point
  1519. * @param int $maxPosition maximum internal position of size unit, ignored if $formatBase is an array
  1520. * @param array|int $formatBase the base at which each next unit is calculated, either 1000 or 1024, or an array
  1521. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1522. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1523. * @return array [parameters for Yii::t containing formatted number, internal position of size unit]
  1524. * @throws InvalidArgumentException if the input value is not numeric or the formatting failed.
  1525. */
  1526. private function formatNumber($value, $decimals, $maxPosition, $formatBase, $options, $textOptions)
  1527. {
  1528. $value = $this->normalizeNumericValue($value);
  1529. $position = 0;
  1530. if (is_array($formatBase)) {
  1531. $maxPosition = count($formatBase) - 1;
  1532. }
  1533. do {
  1534. if (is_array($formatBase)) {
  1535. if (!isset($formatBase[$position + 1])) {
  1536. break;
  1537. }
  1538. if (abs($value) < $formatBase[$position + 1]) {
  1539. break;
  1540. }
  1541. } else {
  1542. if (abs($value) < $formatBase) {
  1543. break;
  1544. }
  1545. $value /= $formatBase;
  1546. }
  1547. $position++;
  1548. } while ($position < $maxPosition + 1);
  1549. if (is_array($formatBase) && $position !== 0) {
  1550. $value /= $formatBase[$position];
  1551. }
  1552. // no decimals for smallest unit
  1553. if ($position === 0) {
  1554. $decimals = 0;
  1555. } elseif ($decimals !== null) {
  1556. $value = round($value, $decimals);
  1557. }
  1558. // disable grouping for edge cases like 1023 to get 1023 B instead of 1,023 B
  1559. $oldThousandSeparator = $this->thousandSeparator;
  1560. $this->thousandSeparator = '';
  1561. if ($this->_intlLoaded && !isset($options[NumberFormatter::GROUPING_USED])) {
  1562. $options[NumberFormatter::GROUPING_USED] = false;
  1563. }
  1564. // format the size value
  1565. $params = [
  1566. // this is the unformatted number used for the plural rule
  1567. // abs() to make sure the plural rules work correctly on negative numbers, intl does not cover this
  1568. // http://english.stackexchange.com/questions/9735/is-1-singular-or-plural
  1569. 'n' => abs($value),
  1570. // this is the formatted number used for display
  1571. 'nFormatted' => $this->asDecimal($value, $decimals, $options, $textOptions),
  1572. ];
  1573. $this->thousandSeparator = $oldThousandSeparator;
  1574. return [$params, $position];
  1575. }
  1576. /**
  1577. * Normalizes a numeric input value.
  1578. *
  1579. * - everything [empty](https://secure.php.net/manual/en/function.empty.php) will result in `0`
  1580. * - a [numeric](https://secure.php.net/manual/en/function.is-numeric.php) string will be casted to float
  1581. * - everything else will be returned if it is [numeric](https://secure.php.net/manual/en/function.is-numeric.php),
  1582. * otherwise an exception is thrown.
  1583. *
  1584. * @param mixed $value the input value
  1585. * @return float|int the normalized number value
  1586. * @throws InvalidArgumentException if the input value is not numeric.
  1587. */
  1588. protected function normalizeNumericValue($value)
  1589. {
  1590. if (empty($value)) {
  1591. return 0;
  1592. }
  1593. if (is_string($value) && is_numeric($value)) {
  1594. $value = (float) $value;
  1595. }
  1596. if (!is_numeric($value)) {
  1597. throw new InvalidArgumentException("'$value' is not a numeric value.");
  1598. }
  1599. return $value;
  1600. }
  1601. /**
  1602. * Creates a number formatter based on the given type and format.
  1603. *
  1604. * You may override this method to create a number formatter based on patterns.
  1605. *
  1606. * @param int $style the type of the number formatter.
  1607. * Values: NumberFormatter::DECIMAL, ::CURRENCY, ::PERCENT, ::SCIENTIFIC, ::SPELLOUT, ::ORDINAL
  1608. * ::DURATION, ::PATTERN_RULEBASED, ::DEFAULT_STYLE, ::IGNORE
  1609. * @param int $decimals the number of digits after the decimal point.
  1610. * @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
  1611. * @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
  1612. * @return NumberFormatter the created formatter instance
  1613. */
  1614. protected function createNumberFormatter($style, $decimals = null, $options = [], $textOptions = [])
  1615. {
  1616. $formatter = new NumberFormatter($this->locale, $style);
  1617. // set text attributes
  1618. foreach ($this->numberFormatterTextOptions as $name => $attribute) {
  1619. $formatter->setTextAttribute($name, $attribute);
  1620. }
  1621. foreach ($textOptions as $name => $attribute) {
  1622. $formatter->setTextAttribute($name, $attribute);
  1623. }
  1624. // set attributes
  1625. foreach ($this->numberFormatterOptions as $name => $value) {
  1626. $formatter->setAttribute($name, $value);
  1627. }
  1628. foreach ($options as $name => $value) {
  1629. $formatter->setAttribute($name, $value);
  1630. }
  1631. if ($decimals !== null) {
  1632. $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $decimals);
  1633. $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $decimals);
  1634. }
  1635. // set symbols
  1636. if ($this->decimalSeparator !== null) {
  1637. $formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, $this->decimalSeparator);
  1638. }
  1639. if ($this->thousandSeparator !== null) {
  1640. $formatter->setSymbol(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
  1641. $formatter->setSymbol(NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, $this->thousandSeparator);
  1642. }
  1643. foreach ($this->numberFormatterSymbols as $name => $symbol) {
  1644. $formatter->setSymbol($name, $symbol);
  1645. }
  1646. return $formatter;
  1647. }
  1648. /**
  1649. * Checks if string representations of given value and its normalized version are different.
  1650. * @param string|float|int $value
  1651. * @param float|int $normalizedValue
  1652. * @return bool
  1653. * @since 2.0.16
  1654. */
  1655. protected function isNormalizedValueMispresented($value, $normalizedValue)
  1656. {
  1657. if (empty($value)) {
  1658. $value = 0;
  1659. }
  1660. return (string) $normalizedValue !== $this->normalizeNumericStringValue((string) $value);
  1661. }
  1662. /**
  1663. * Normalizes a numeric string value.
  1664. * @param string $value
  1665. * @return string the normalized number value as a string
  1666. * @since 2.0.16
  1667. */
  1668. protected function normalizeNumericStringValue($value)
  1669. {
  1670. $powerPosition = strrpos($value, 'E');
  1671. if ($powerPosition !== false) {
  1672. $valuePart = substr($value, 0, $powerPosition);
  1673. $powerPart = substr($value, $powerPosition + 1);
  1674. } else {
  1675. $powerPart = null;
  1676. $valuePart = $value;
  1677. }
  1678. $separatorPosition = strrpos($valuePart, '.');
  1679. if ($separatorPosition !== false) {
  1680. $integerPart = substr($valuePart, 0, $separatorPosition);
  1681. $fractionalPart = substr($valuePart, $separatorPosition + 1);
  1682. } else {
  1683. $integerPart = $valuePart;
  1684. $fractionalPart = null;
  1685. }
  1686. // truncate insignificant zeros, keep minus
  1687. $integerPart = preg_replace('/^\+?(-?)0*(\d+)$/', '$1$2', $integerPart);
  1688. // for zeros only leave one zero, keep minus
  1689. $integerPart = preg_replace('/^\+?(-?)0*$/', '${1}0', $integerPart);
  1690. if ($fractionalPart !== null) {
  1691. // truncate insignificant zeros
  1692. $fractionalPart = rtrim($fractionalPart, '0');
  1693. if (empty($fractionalPart)) {
  1694. $fractionalPart = $powerPart !== null ? '0' : null;
  1695. }
  1696. }
  1697. $normalizedValue = $integerPart;
  1698. if ($fractionalPart !== null) {
  1699. $normalizedValue .= '.' . $fractionalPart;
  1700. } elseif ($normalizedValue === '-0') {
  1701. $normalizedValue = '0';
  1702. }
  1703. if ($powerPart !== null) {
  1704. $normalizedValue .= 'E' . $powerPart;
  1705. }
  1706. return $normalizedValue;
  1707. }
  1708. /**
  1709. * Fallback for formatting value as a decimal number.
  1710. *
  1711. * Property [[decimalSeparator]] will be used to represent the decimal point. The value is rounded automatically
  1712. * to the defined decimal digits.
  1713. *
  1714. * @param string|int|float $value the value to be formatted.
  1715. * @param int $decimals the number of digits after the decimal point. The default value is `2`.
  1716. * @return string the formatted result.
  1717. * @see decimalSeparator
  1718. * @see thousandSeparator
  1719. * @since 2.0.16
  1720. */
  1721. protected function asDecimalStringFallback($value, $decimals = 2)
  1722. {
  1723. if (empty($value)) {
  1724. $value = 0;
  1725. }
  1726. $value = $this->normalizeNumericStringValue((string) $value);
  1727. $separatorPosition = strrpos($value, '.');
  1728. if ($separatorPosition !== false) {
  1729. $integerPart = substr($value, 0, $separatorPosition);
  1730. $fractionalPart = substr($value, $separatorPosition + 1);
  1731. } else {
  1732. $integerPart = $value;
  1733. $fractionalPart = null;
  1734. }
  1735. $decimalOutput = '';
  1736. if ($decimals === null) {
  1737. $decimals = 2;
  1738. }
  1739. $carry = 0;
  1740. if ($decimals > 0) {
  1741. $decimalSeparator = $this->decimalSeparator;
  1742. if ($this->decimalSeparator === null) {
  1743. $decimalSeparator = '.';
  1744. }
  1745. if ($fractionalPart === null) {
  1746. $fractionalPart = str_repeat('0', $decimals);
  1747. } elseif (strlen($fractionalPart) > $decimals) {
  1748. $cursor = $decimals;
  1749. // checking if fractional part must be rounded
  1750. if ((int) substr($fractionalPart, $cursor, 1) >= 5) {
  1751. while (--$cursor >= 0) {
  1752. $carry = 0;
  1753. $oneUp = (int) substr($fractionalPart, $cursor, 1) + 1;
  1754. if ($oneUp === 10) {
  1755. $oneUp = 0;
  1756. $carry = 1;
  1757. }
  1758. $fractionalPart = substr($fractionalPart, 0, $cursor) . $oneUp . substr($fractionalPart, $cursor + 1);
  1759. if ($carry === 0) {
  1760. break;
  1761. }
  1762. }
  1763. }
  1764. $fractionalPart = substr($fractionalPart, 0, $decimals);
  1765. } elseif (strlen($fractionalPart) < $decimals) {
  1766. $fractionalPart = str_pad($fractionalPart, $decimals, '0');
  1767. }
  1768. $decimalOutput .= $decimalSeparator . $fractionalPart;
  1769. }
  1770. // checking if integer part must be rounded
  1771. if ($carry || ($decimals === 0 && $fractionalPart !== null && (int) substr($fractionalPart, 0, 1) >= 5)) {
  1772. $integerPartLength = strlen($integerPart);
  1773. $cursor = 0;
  1774. while (++$cursor <= $integerPartLength) {
  1775. $carry = 0;
  1776. $oneUp = (int) substr($integerPart, -$cursor, 1) + 1;
  1777. if ($oneUp === 10) {
  1778. $oneUp = 0;
  1779. $carry = 1;
  1780. }
  1781. $integerPart = substr($integerPart, 0, -$cursor) . $oneUp . substr($integerPart, $integerPartLength - $cursor + 1);
  1782. if ($carry === 0) {
  1783. break;
  1784. }
  1785. }
  1786. if ($carry === 1) {
  1787. $integerPart = '1' . $integerPart;
  1788. }
  1789. }
  1790. if (strlen($integerPart) > 3) {
  1791. $thousandSeparator = $this->thousandSeparator;
  1792. if ($thousandSeparator === null) {
  1793. $thousandSeparator = ',';
  1794. }
  1795. $integerPart = strrev(implode(',', str_split(strrev($integerPart), 3)));
  1796. if ($thousandSeparator !== ',') {
  1797. $integerPart = str_replace(',', $thousandSeparator, $integerPart);
  1798. }
  1799. }
  1800. return $integerPart . $decimalOutput;
  1801. }
  1802. /**
  1803. * Fallback for formatting value as an integer number by removing any decimal digits without rounding.
  1804. *
  1805. * @param string|int|float $value the value to be formatted.
  1806. * @return string the formatted result.
  1807. * @since 2.0.16
  1808. */
  1809. protected function asIntegerStringFallback($value)
  1810. {
  1811. if (empty($value)) {
  1812. $value = 0;
  1813. }
  1814. $value = $this->normalizeNumericStringValue((string) $value);
  1815. $separatorPosition = strrpos($value, '.');
  1816. if ($separatorPosition !== false) {
  1817. $integerPart = substr($value, 0, $separatorPosition);
  1818. } else {
  1819. $integerPart = $value;
  1820. }
  1821. return $this->asDecimalStringFallback($integerPart, 0);
  1822. }
  1823. /**
  1824. * Fallback for formatting value as a percent number with "%" sign.
  1825. *
  1826. * Property [[decimalSeparator]] will be used to represent the decimal point. The value is rounded automatically
  1827. * to the defined decimal digits.
  1828. *
  1829. * @param string|int|float $value the value to be formatted.
  1830. * @param int $decimals the number of digits after the decimal point. The default value is `0`.
  1831. * @return string the formatted result.
  1832. * @since 2.0.16
  1833. */
  1834. protected function asPercentStringFallback($value, $decimals = null)
  1835. {
  1836. if (empty($value)) {
  1837. $value = 0;
  1838. }
  1839. if ($decimals === null) {
  1840. $decimals = 0;
  1841. }
  1842. $value = $this->normalizeNumericStringValue((string) $value);
  1843. $separatorPosition = strrpos($value, '.');
  1844. if ($separatorPosition !== false) {
  1845. $integerPart = substr($value, 0, $separatorPosition);
  1846. $fractionalPart = str_pad(substr($value, $separatorPosition + 1), 2, '0');
  1847. $integerPart .= substr($fractionalPart, 0, 2);
  1848. $fractionalPart = substr($fractionalPart, 2);
  1849. if ($fractionalPart === '') {
  1850. $multipliedValue = $integerPart;
  1851. } else {
  1852. $multipliedValue = $integerPart . '.' . $fractionalPart;
  1853. }
  1854. } else {
  1855. $multipliedValue = $value . '00';
  1856. }
  1857. return $this->asDecimalStringFallback($multipliedValue, $decimals) . '%';
  1858. }
  1859. /**
  1860. * Fallback for formatting value as a currency number.
  1861. *
  1862. * @param string|int|float $value the value to be formatted.
  1863. * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use.
  1864. * If null, [[currencyCode]] will be used.
  1865. * @return string the formatted result.
  1866. * @throws InvalidConfigException if no currency is given and [[currencyCode]] is not defined.
  1867. * @since 2.0.16
  1868. */
  1869. protected function asCurrencyStringFallback($value, $currency = null)
  1870. {
  1871. if ($currency === null) {
  1872. if ($this->currencyCode === null) {
  1873. throw new InvalidConfigException('The default currency code for the formatter is not defined.');
  1874. }
  1875. $currency = $this->currencyCode;
  1876. }
  1877. return $currency . ' ' . $this->asDecimalStringFallback($value, 2);
  1878. }
  1879. }