1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://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. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 0.10.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\I18n;
16:
17: use NumberFormatter;
18:
19: /**
20: * Number helper library.
21: *
22: * Methods to make numbers more readable.
23: *
24: * @link https://book.cakephp.org/3.0/en/core-libraries/number.html
25: */
26: class Number
27: {
28:
29: /**
30: * Default locale
31: *
32: * @var string
33: */
34: const DEFAULT_LOCALE = 'en_US';
35:
36: /**
37: * Format type to format as currency
38: *
39: * @var string
40: */
41: const FORMAT_CURRENCY = 'currency';
42:
43: /**
44: * A list of number formatters indexed by locale and type
45: *
46: * @var array
47: */
48: protected static $_formatters = [];
49:
50: /**
51: * Default currency used by Number::currency()
52: *
53: * @var string|null
54: */
55: protected static $_defaultCurrency;
56:
57: /**
58: * Formats a number with a level of precision.
59: *
60: * Options:
61: *
62: * - `locale`: The locale name to use for formatting the number, e.g. fr_FR
63: *
64: * @param float $value A floating point number.
65: * @param int $precision The precision of the returned number.
66: * @param array $options Additional options
67: * @return string Formatted float.
68: * @link https://book.cakephp.org/3.0/en/core-libraries/number.html#formatting-floating-point-numbers
69: */
70: public static function precision($value, $precision = 3, array $options = [])
71: {
72: $formatter = static::formatter(['precision' => $precision, 'places' => $precision] + $options);
73:
74: return $formatter->format($value);
75: }
76:
77: /**
78: * Returns a formatted-for-humans file size.
79: *
80: * @param int $size Size in bytes
81: * @return string Human readable size
82: * @link https://book.cakephp.org/3.0/en/core-libraries/number.html#interacting-with-human-readable-values
83: */
84: public static function toReadableSize($size)
85: {
86: switch (true) {
87: case $size < 1024:
88: return __dn('cake', '{0,number,integer} Byte', '{0,number,integer} Bytes', $size, $size);
89: case round($size / 1024) < 1024:
90: return __d('cake', '{0,number,#,###.##} KB', $size / 1024);
91: case round($size / 1024 / 1024, 2) < 1024:
92: return __d('cake', '{0,number,#,###.##} MB', $size / 1024 / 1024);
93: case round($size / 1024 / 1024 / 1024, 2) < 1024:
94: return __d('cake', '{0,number,#,###.##} GB', $size / 1024 / 1024 / 1024);
95: default:
96: return __d('cake', '{0,number,#,###.##} TB', $size / 1024 / 1024 / 1024 / 1024);
97: }
98: }
99:
100: /**
101: * Formats a number into a percentage string.
102: *
103: * Options:
104: *
105: * - `multiply`: Multiply the input value by 100 for decimal percentages.
106: * - `locale`: The locale name to use for formatting the number, e.g. fr_FR
107: *
108: * @param float $value A floating point number
109: * @param int $precision The precision of the returned number
110: * @param array $options Options
111: * @return string Percentage string
112: * @link https://book.cakephp.org/3.0/en/core-libraries/number.html#formatting-percentages
113: */
114: public static function toPercentage($value, $precision = 2, array $options = [])
115: {
116: $options += ['multiply' => false];
117: if ($options['multiply']) {
118: $value *= 100;
119: }
120:
121: return static::precision($value, $precision, $options) . '%';
122: }
123:
124: /**
125: * Formats a number into the correct locale format
126: *
127: * Options:
128: *
129: * - `places` - Minimum number or decimals to use, e.g 0
130: * - `precision` - Maximum Number of decimal places to use, e.g. 2
131: * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
132: * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
133: * - `before` - The string to place before whole numbers, e.g. '['
134: * - `after` - The string to place after decimal numbers, e.g. ']'
135: *
136: * @param float $value A floating point number.
137: * @param array $options An array with options.
138: * @return string Formatted number
139: */
140: public static function format($value, array $options = [])
141: {
142: $formatter = static::formatter($options);
143: $options += ['before' => '', 'after' => ''];
144:
145: return $options['before'] . $formatter->format($value) . $options['after'];
146: }
147:
148: /**
149: * Parse a localized numeric string and transform it in a float point
150: *
151: * Options:
152: *
153: * - `locale` - The locale name to use for parsing the number, e.g. fr_FR
154: * - `type` - The formatter type to construct, set it to `currency` if you need to parse
155: * numbers representing money.
156: *
157: * @param string $value A numeric string.
158: * @param array $options An array with options.
159: * @return float point number
160: */
161: public static function parseFloat($value, array $options = [])
162: {
163: $formatter = static::formatter($options);
164:
165: return (float)$formatter->parse($value, NumberFormatter::TYPE_DOUBLE);
166: }
167:
168: /**
169: * Formats a number into the correct locale format to show deltas (signed differences in value).
170: *
171: * ### Options
172: *
173: * - `places` - Minimum number or decimals to use, e.g 0
174: * - `precision` - Maximum Number of decimal places to use, e.g. 2
175: * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
176: * - `before` - The string to place before whole numbers, e.g. '['
177: * - `after` - The string to place after decimal numbers, e.g. ']'
178: *
179: * @param float $value A floating point number
180: * @param array $options Options list.
181: * @return string formatted delta
182: */
183: public static function formatDelta($value, array $options = [])
184: {
185: $options += ['places' => 0];
186: $value = number_format($value, $options['places'], '.', '');
187: $sign = $value > 0 ? '+' : '';
188: $options['before'] = isset($options['before']) ? $options['before'] . $sign : $sign;
189:
190: return static::format($value, $options);
191: }
192:
193: /**
194: * Formats a number into a currency format.
195: *
196: * ### Options
197: *
198: * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
199: * - `fractionSymbol` - The currency symbol to use for fractional numbers.
200: * - `fractionPosition` - The position the fraction symbol should be placed
201: * valid options are 'before' & 'after'.
202: * - `before` - Text to display before the rendered number
203: * - `after` - Text to display after the rendered number
204: * - `zero` - The text to use for zero values, can be a string or a number. e.g. 0, 'Free!'
205: * - `places` - Number of decimal places to use. e.g. 2
206: * - `precision` - Maximum Number of decimal places to use, e.g. 2
207: * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
208: * - `useIntlCode` - Whether or not to replace the currency symbol with the international
209: * currency code.
210: *
211: * @param float $value Value to format.
212: * @param string|null $currency International currency name such as 'USD', 'EUR', 'JPY', 'CAD'
213: * @param array $options Options list.
214: * @return string Number formatted as a currency.
215: */
216: public static function currency($value, $currency = null, array $options = [])
217: {
218: $value = (float)$value;
219: $currency = $currency ?: static::defaultCurrency();
220:
221: if (isset($options['zero']) && !$value) {
222: return $options['zero'];
223: }
224:
225: $formatter = static::formatter(['type' => static::FORMAT_CURRENCY] + $options);
226: $abs = abs($value);
227: if (!empty($options['fractionSymbol']) && $abs > 0 && $abs < 1) {
228: $value *= 100;
229: $pos = isset($options['fractionPosition']) ? $options['fractionPosition'] : 'after';
230:
231: return static::format($value, ['precision' => 0, $pos => $options['fractionSymbol']]);
232: }
233:
234: $before = isset($options['before']) ? $options['before'] : null;
235: $after = isset($options['after']) ? $options['after'] : null;
236:
237: return $before . $formatter->formatCurrency($value, $currency) . $after;
238: }
239:
240: /**
241: * Getter/setter for default currency
242: *
243: * @param string|bool|null $currency Default currency string to be used by currency()
244: * if $currency argument is not provided. If boolean false is passed, it will clear the
245: * currently stored value
246: * @return string|null Currency
247: */
248: public static function defaultCurrency($currency = null)
249: {
250: if (!empty($currency)) {
251: return self::$_defaultCurrency = $currency;
252: }
253:
254: if ($currency === false) {
255: return self::$_defaultCurrency = null;
256: }
257:
258: if (empty(self::$_defaultCurrency)) {
259: $locale = ini_get('intl.default_locale') ?: static::DEFAULT_LOCALE;
260: $formatter = new NumberFormatter($locale, NumberFormatter::CURRENCY);
261: self::$_defaultCurrency = $formatter->getTextAttribute(NumberFormatter::CURRENCY_CODE);
262: }
263:
264: return self::$_defaultCurrency;
265: }
266:
267: /**
268: * Returns a formatter object that can be reused for similar formatting task
269: * under the same locale and options. This is often a speedier alternative to
270: * using other methods in this class as only one formatter object needs to be
271: * constructed.
272: *
273: * ### Options
274: *
275: * - `locale` - The locale name to use for formatting the number, e.g. fr_FR
276: * - `type` - The formatter type to construct, set it to `currency` if you need to format
277: * numbers representing money or a NumberFormatter constant.
278: * - `places` - Number of decimal places to use. e.g. 2
279: * - `precision` - Maximum Number of decimal places to use, e.g. 2
280: * - `pattern` - An ICU number pattern to use for formatting the number. e.g #,##0.00
281: * - `useIntlCode` - Whether or not to replace the currency symbol with the international
282: * currency code.
283: *
284: * @param array $options An array with options.
285: * @return \NumberFormatter The configured formatter instance
286: */
287: public static function formatter($options = [])
288: {
289: $locale = isset($options['locale']) ? $options['locale'] : ini_get('intl.default_locale');
290:
291: if (!$locale) {
292: $locale = static::DEFAULT_LOCALE;
293: }
294:
295: $type = NumberFormatter::DECIMAL;
296: if (!empty($options['type'])) {
297: $type = $options['type'];
298: if ($options['type'] === static::FORMAT_CURRENCY) {
299: $type = NumberFormatter::CURRENCY;
300: }
301: }
302:
303: if (!isset(static::$_formatters[$locale][$type])) {
304: static::$_formatters[$locale][$type] = new NumberFormatter($locale, $type);
305: }
306:
307: $formatter = static::$_formatters[$locale][$type];
308:
309: $options = array_intersect_key($options, [
310: 'places' => null,
311: 'precision' => null,
312: 'pattern' => null,
313: 'useIntlCode' => null
314: ]);
315: if (empty($options)) {
316: return $formatter;
317: }
318:
319: $formatter = clone $formatter;
320:
321: return static::_setAttributes($formatter, $options);
322: }
323:
324: /**
325: * Configure formatters.
326: *
327: * @param string $locale The locale name to use for formatting the number, e.g. fr_FR
328: * @param int $type The formatter type to construct. Defaults to NumberFormatter::DECIMAL.
329: * @param array $options See Number::formatter() for possible options.
330: * @return void
331: */
332: public static function config($locale, $type = NumberFormatter::DECIMAL, array $options = [])
333: {
334: static::$_formatters[$locale][$type] = static::_setAttributes(
335: new NumberFormatter($locale, $type),
336: $options
337: );
338: }
339:
340: /**
341: * Set formatter attributes
342: *
343: * @param \NumberFormatter $formatter Number formatter instance.
344: * @param array $options See Number::formatter() for possible options.
345: * @return \NumberFormatter
346: */
347: protected static function _setAttributes(NumberFormatter $formatter, array $options = [])
348: {
349: if (isset($options['places'])) {
350: $formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $options['places']);
351: }
352:
353: if (isset($options['precision'])) {
354: $formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $options['precision']);
355: }
356:
357: if (!empty($options['pattern'])) {
358: $formatter->setPattern($options['pattern']);
359: }
360:
361: if (!empty($options['useIntlCode'])) {
362: // One of the odd things about ICU is that the currency marker in patterns
363: // is denoted with ¤, whereas the international code is marked with ¤¤,
364: // in order to use the code we need to simply duplicate the character wherever
365: // it appears in the pattern.
366: $pattern = trim(str_replace('¤', '¤¤ ', $formatter->getPattern()));
367: $formatter->setPattern($pattern);
368: }
369:
370: return $formatter;
371: }
372:
373: /**
374: * Returns a formatted integer as an ordinal number string (e.g. 1st, 2nd, 3rd, 4th, [...])
375: *
376: * ### Options
377: *
378: * - `type` - The formatter type to construct, set it to `currency` if you need to format
379: * numbers representing money or a NumberFormatter constant.
380: *
381: * For all other options see formatter().
382: *
383: * @param int|float $value An integer
384: * @param array $options An array with options.
385: * @return string
386: */
387: public static function ordinal($value, array $options = [])
388: {
389: return static::formatter(['type' => NumberFormatter::ORDINAL] + $options)->format($value);
390: }
391: }
392: