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