Number.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 0.10.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Utility;
  16. use Cake\Error\Exception;
  17. use NumberFormatter;
  18. /**
  19. * Number helper library.
  20. *
  21. * Methods to make numbers more readable.
  22. *
  23. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html
  24. */
  25. class Number {
  26. /**
  27. * Currencies supported by the helper. You can add additional currency formats
  28. * with Cake\Utility\Number::addFormat
  29. *
  30. * @var array
  31. */
  32. protected static $_currencies = array(
  33. 'AUD' => array(
  34. 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after',
  35. 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true,
  36. 'fractionExponent' => 2
  37. ),
  38. 'CAD' => array(
  39. 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after',
  40. 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true,
  41. 'fractionExponent' => 2
  42. ),
  43. 'USD' => array(
  44. 'wholeSymbol' => '$', 'wholePosition' => 'before', 'fractionSymbol' => 'c', 'fractionPosition' => 'after',
  45. 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true,
  46. 'fractionExponent' => 2
  47. ),
  48. 'EUR' => array(
  49. 'wholeSymbol' => '€', 'wholePosition' => 'before', 'fractionSymbol' => false, 'fractionPosition' => 'after',
  50. 'zero' => 0, 'places' => 2, 'thousands' => '.', 'decimals' => ',', 'negative' => '()', 'escape' => true,
  51. 'fractionExponent' => 0
  52. ),
  53. 'GBP' => array(
  54. 'wholeSymbol' => '£', 'wholePosition' => 'before', 'fractionSymbol' => 'p', 'fractionPosition' => 'after',
  55. 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true,
  56. 'fractionExponent' => 2
  57. ),
  58. 'JPY' => array(
  59. 'wholeSymbol' => '¥', 'wholePosition' => 'before', 'fractionSymbol' => false, 'fractionPosition' => 'after',
  60. 'zero' => 0, 'places' => 2, 'thousands' => ',', 'decimals' => '.', 'negative' => '()', 'escape' => true,
  61. 'fractionExponent' => 0
  62. ),
  63. );
  64. /**
  65. * A list of number formatters indexed by locale
  66. *
  67. * @var array
  68. */
  69. protected static $_formatters = [];
  70. /**
  71. * A list of currency formatters indexed by locale
  72. *
  73. * @var array
  74. */
  75. protected static $_currencyFormatters = [];
  76. /**
  77. * Default currency used by Number::currency()
  78. *
  79. * @var string
  80. */
  81. protected static $_defaultCurrency;
  82. /**
  83. * Formats a number with a level of precision.
  84. *
  85. * @param float $value A floating point number.
  86. * @param int $precision The precision of the returned number.
  87. * @return float Formatted float.
  88. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::precision
  89. */
  90. public static function precision($value, $precision = 3) {
  91. $locale = ini_get('intl.default_locale') ?: 'en_US';
  92. if (!isset(static::$_formatters[$locale])) {
  93. static::$_formatters[$locale] = new NumberFormatter($locale, NumberFormatter::DECIMAL);
  94. }
  95. $formatter = static::$_formatters[$locale];
  96. $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $precision);
  97. $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $precision);
  98. return $formatter->format($value);
  99. }
  100. /**
  101. * Returns a formatted-for-humans file size.
  102. *
  103. * @param int $size Size in bytes
  104. * @return string Human readable size
  105. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::toReadableSize
  106. */
  107. public static function toReadableSize($size) {
  108. switch (true) {
  109. case $size < 1024:
  110. return __dn('cake', '{0,number,integer} Byte', '{0,number,integer} Bytes', $size, $size);
  111. case round($size / 1024) < 1024:
  112. return __d('cake', '{0,number,#,###.##} KB', $size / 1024);
  113. case round($size / 1024 / 1024, 2) < 1024:
  114. return __d('cake', '{0,number,#,###.##} MB', $size / 1024 / 1024);
  115. case round($size / 1024 / 1024 / 1024, 2) < 1024:
  116. return __d('cake', '{0,number,#,###.##} GB', $size / 1024 / 1024 / 1024);
  117. default:
  118. return __d('cake', '{0,number,#,###.##} TB', $size / 1024 / 1024 / 1024 / 1024);
  119. }
  120. }
  121. /**
  122. * Converts filesize from human readable string to bytes
  123. *
  124. * @param string $size Size in human readable string like '5MB', '5M', '500B', '50kb' etc.
  125. * @param mixed $default Value to be returned when invalid size was used, for example 'Unknown type'
  126. * @return mixed Number of bytes as integer on success, `$default` on failure if not false
  127. * @throws \Cake\Error\Exception On invalid Unit type.
  128. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::fromReadableSize
  129. */
  130. public static function fromReadableSize($size, $default = false) {
  131. if (ctype_digit($size)) {
  132. return (int)$size;
  133. }
  134. $size = strtoupper($size);
  135. $l = -2;
  136. $i = array_search(substr($size, -2), array('KB', 'MB', 'GB', 'TB', 'PB'));
  137. if ($i === false) {
  138. $l = -1;
  139. $i = array_search(substr($size, -1), array('K', 'M', 'G', 'T', 'P'));
  140. }
  141. if ($i !== false) {
  142. $size = substr($size, 0, $l);
  143. return $size * pow(1024, $i + 1);
  144. }
  145. if (substr($size, -1) === 'B' && ctype_digit(substr($size, 0, -1))) {
  146. $size = substr($size, 0, -1);
  147. return (int)$size;
  148. }
  149. if ($default !== false) {
  150. return $default;
  151. }
  152. throw new Exception('No unit type.');
  153. }
  154. /**
  155. * Formats a number into a percentage string.
  156. *
  157. * Options:
  158. *
  159. * - `multiply`: Multiply the input value by 100 for decimal percentages.
  160. *
  161. * @param float $value A floating point number
  162. * @param int $precision The precision of the returned number
  163. * @param array $options Options
  164. * @return string Percentage string
  165. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::toPercentage
  166. */
  167. public static function toPercentage($value, $precision = 2, array $options = array()) {
  168. $options += array('multiply' => false);
  169. if ($options['multiply']) {
  170. $value *= 100;
  171. }
  172. return static::precision($value, $precision) . '%';
  173. }
  174. /**
  175. * Formats a number into the correct locale format
  176. *
  177. * Options:
  178. *
  179. * - `places` - Minimim number or decimals to use, e.g 0
  180. * - `precision` - Maximum Number of decimal places to use, e.g. 2
  181. * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
  182. * - `before` - The string to place before whole numbers, e.g. '['
  183. * - `after` - The string to place after decimal numbers, e.g. ']'
  184. * - `escape` - Set to false to prevent escaping
  185. *
  186. * @param float $value A floating point number.
  187. * @param array $options An array with options.
  188. * @return string Formatted number
  189. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::format
  190. */
  191. public static function format($value, array $options = []) {
  192. $locale = isset($options['locale']) ? $options['locale'] : ini_get('intl.default_locale');
  193. if (!$locale) {
  194. $locale = 'en_US';
  195. }
  196. if (!isset(static::$_formatters[$locale])) {
  197. static::$_formatters[$locale] = new NumberFormatter($locale, NumberFormatter::DECIMAL);
  198. }
  199. $formatter = static::$_formatters[$locale];
  200. $map = [
  201. 'places' => NumberFormatter::MIN_FRACTION_DIGITS,
  202. 'precision' => NumberFormatter::MAX_FRACTION_DIGITS
  203. ];
  204. foreach ($map as $opt => $setting) {
  205. if (isset($options[$opt])) {
  206. $formatter->setAttribute($setting, $options[$opt]);
  207. }
  208. }
  209. $options += ['before' => '', 'after' => '', 'escape' => true];
  210. $out = $options['before'] . $formatter->format($value) . $options['after'];
  211. if (!empty($options['escape'])) {
  212. return h($out);
  213. }
  214. return $out;
  215. }
  216. /**
  217. * Formats a number into the correct locale format to show deltas (signed differences in value).
  218. *
  219. * ### Options
  220. *
  221. * - `places` - Minimim number or decimals to use, e.g 0
  222. * - `precision` - Maximum Number of decimal places to use, e.g. 2
  223. * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
  224. * - `before` - The string to place before whole numbers, e.g. '['
  225. * - `after` - The string to place after decimal numbers, e.g. ']'
  226. * - `escape` - Set to false to prevent escaping
  227. *
  228. * @param float $value A floating point number
  229. * @param array $options Options list.
  230. * @return string formatted delta
  231. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::formatDelta
  232. */
  233. public static function formatDelta($value, array $options = array()) {
  234. $options += ['places' => 0];
  235. $value = number_format($value, $options['places'], '.', '');
  236. $sign = $value > 0 ? '+' : '';
  237. $options['before'] = isset($options['before']) ? $options['before'] . $sign : $sign;
  238. return static::format($value, $options);
  239. }
  240. /**
  241. * Formats a number into a currency format.
  242. *
  243. * ### Options
  244. *
  245. * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
  246. * - `fractionSymbol` - The currency symbol to use for fractional numbers.
  247. * - `fractionPosition` - The position the fraction symbol should be placed
  248. * valid options are 'before' & 'after'.
  249. * - `before` - Text to display before the rendered number
  250. * - `after` - Text to display after the rendered number
  251. * - `zero` - The text to use for zero values, can be a string or a number. e.g. 0, 'Free!'
  252. * - `places` - Number of decimal places to use. e.g. 2
  253. * - `precision` - Maximum Number of decimal places to use, e.g. 2
  254. * - `pattern` - An ICU number patter to use for formatting the number. e.g #,###.00
  255. *
  256. * @param float $value Value to format.
  257. * @param string $currency International currency name such as 'USD', 'EUR', 'JPY', 'CAD'
  258. * @param array $options Options list.
  259. * @return string Number formatted as a currency.
  260. */
  261. public static function currency($value, $currency = null, array $options = array()) {
  262. $value = (float)$value;
  263. $currency = $currency ?: static::defaultCurrency();
  264. if (isset($options['zero']) && !$value) {
  265. return $options['zero'];
  266. }
  267. $locale = isset($options['locale']) ? $options['locale'] : ini_get('intl.default_locale');
  268. if (!$locale) {
  269. $locale = 'en_US';
  270. }
  271. if (!isset(static::$_currencyFormatters[$locale])) {
  272. static::$_currencyFormatters[$locale] = new NumberFormatter(
  273. $locale,
  274. NumberFormatter::CURRENCY
  275. );
  276. }
  277. $formatter = static::$_currencyFormatters[$locale];
  278. $hasPlaces = isset($options['places']);
  279. $hasPrecision = isset($options['precision']);
  280. $hasPattern = !empty($options['pattern']);
  281. if ($hasPlaces || $hasPrecision || $hasPattern) {
  282. $formatter = clone $formatter;
  283. }
  284. if ($hasPlaces) {
  285. $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $options['places']);
  286. }
  287. if ($hasPrecision) {
  288. $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $options['precision']);
  289. }
  290. if ($hasPattern) {
  291. $formatter->setPattern($options['pattern']);
  292. }
  293. $abs = abs($value);
  294. if (!empty($options['fractionSymbol']) && $abs > 0 && $abs < 1) {
  295. $value = $value * 100;
  296. $pos = isset($options['fractionPosition']) ? $options['fractionPosition'] : 'after';
  297. return static::format($value, ['precision' => 0, $pos => $options['fractionSymbol']]);
  298. }
  299. $before = isset($options['before']) ? $options['before'] : null;
  300. $after = isset($options['after']) ? $options['after'] : null;
  301. return $before . $formatter->formatCurrency($value, $currency) . $after;
  302. }
  303. /**
  304. * Add a currency format to the Number helper. Makes reusing
  305. * currency formats easier.
  306. *
  307. * {{{ $number->addFormat('NOK', array('before' => 'Kr. ')); }}}
  308. *
  309. * You can now use `NOK` as a shortform when formatting currency amounts.
  310. *
  311. * {{{ $number->currency($value, 'NOK'); }}}
  312. *
  313. * Added formats are merged with the defaults defined in Cake\Utility\Number::$_currencyDefaults
  314. * See Cake\Utility\Number::currency() for more information on the various options and their function.
  315. *
  316. * @param string $formatName The format name to be used in the future.
  317. * @param array $options The array of options for this format.
  318. * @return void
  319. * @see NumberHelper::currency()
  320. * @link http://book.cakephp.org/2.0/en/core-libraries/helpers/number.html#NumberHelper::addFormat
  321. */
  322. public static function addFormat($formatName, array $options) {
  323. static::$_currencies[$formatName] = $options + static::$_currencyDefaults;
  324. }
  325. /**
  326. * Getter/setter for default currency
  327. *
  328. * @param string|boolean $currency Default currency string to be used by currency()
  329. * if $currency argument is not provided. If boolean false is passed, it will clear the
  330. * currently stored value
  331. * @return string Currency
  332. */
  333. public static function defaultCurrency($currency = null) {
  334. if (!empty($currency)) {
  335. return self::$_defaultCurrency = $currency;
  336. }
  337. if ($currency === false) {
  338. self::$_defaultCurrency = null;
  339. }
  340. if (empty(self::$_defaultCurrency)) {
  341. $locale = ini_get('intl.default_locale') ?: 'en_US';
  342. $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
  343. self::$_defaultCurrency = $formatter->getTextAttribute(NumberFormatter::CURRENCY_CODE);
  344. }
  345. return self::$_defaultCurrency;
  346. }
  347. }