Formatter.php 91 KB

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