1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Intl\NumberFormatter; 13 14use Symfony\Component\Intl\Exception\MethodArgumentNotImplementedException; 15use Symfony\Component\Intl\Exception\MethodArgumentValueNotImplementedException; 16use Symfony\Component\Intl\Exception\MethodNotImplementedException; 17use Symfony\Component\Intl\Exception\NotImplementedException; 18use Symfony\Component\Intl\Globals\IntlGlobals; 19use Symfony\Component\Intl\Intl; 20use Symfony\Component\Intl\Locale\Locale; 21 22/** 23 * Replacement for PHP's native {@link \NumberFormatter} class. 24 * 25 * The only methods currently supported in this class are: 26 * 27 * - {@link __construct} 28 * - {@link create} 29 * - {@link formatCurrency} 30 * - {@link format} 31 * - {@link getAttribute} 32 * - {@link getErrorCode} 33 * - {@link getErrorMessage} 34 * - {@link getLocale} 35 * - {@link parse} 36 * - {@link setAttribute} 37 * 38 * @author Eriksen Costa <eriksen.costa@infranology.com.br> 39 * @author Bernhard Schussek <bschussek@gmail.com> 40 * 41 * @internal 42 */ 43class NumberFormatter 44{ 45 /* Format style constants */ 46 const PATTERN_DECIMAL = 0; 47 const DECIMAL = 1; 48 const CURRENCY = 2; 49 const PERCENT = 3; 50 const SCIENTIFIC = 4; 51 const SPELLOUT = 5; 52 const ORDINAL = 6; 53 const DURATION = 7; 54 const PATTERN_RULEBASED = 9; 55 const IGNORE = 0; 56 const DEFAULT_STYLE = 1; 57 58 /* Format type constants */ 59 const TYPE_DEFAULT = 0; 60 const TYPE_INT32 = 1; 61 const TYPE_INT64 = 2; 62 const TYPE_DOUBLE = 3; 63 const TYPE_CURRENCY = 4; 64 65 /* Numeric attribute constants */ 66 const PARSE_INT_ONLY = 0; 67 const GROUPING_USED = 1; 68 const DECIMAL_ALWAYS_SHOWN = 2; 69 const MAX_INTEGER_DIGITS = 3; 70 const MIN_INTEGER_DIGITS = 4; 71 const INTEGER_DIGITS = 5; 72 const MAX_FRACTION_DIGITS = 6; 73 const MIN_FRACTION_DIGITS = 7; 74 const FRACTION_DIGITS = 8; 75 const MULTIPLIER = 9; 76 const GROUPING_SIZE = 10; 77 const ROUNDING_MODE = 11; 78 const ROUNDING_INCREMENT = 12; 79 const FORMAT_WIDTH = 13; 80 const PADDING_POSITION = 14; 81 const SECONDARY_GROUPING_SIZE = 15; 82 const SIGNIFICANT_DIGITS_USED = 16; 83 const MIN_SIGNIFICANT_DIGITS = 17; 84 const MAX_SIGNIFICANT_DIGITS = 18; 85 const LENIENT_PARSE = 19; 86 87 /* Text attribute constants */ 88 const POSITIVE_PREFIX = 0; 89 const POSITIVE_SUFFIX = 1; 90 const NEGATIVE_PREFIX = 2; 91 const NEGATIVE_SUFFIX = 3; 92 const PADDING_CHARACTER = 4; 93 const CURRENCY_CODE = 5; 94 const DEFAULT_RULESET = 6; 95 const PUBLIC_RULESETS = 7; 96 97 /* Format symbol constants */ 98 const DECIMAL_SEPARATOR_SYMBOL = 0; 99 const GROUPING_SEPARATOR_SYMBOL = 1; 100 const PATTERN_SEPARATOR_SYMBOL = 2; 101 const PERCENT_SYMBOL = 3; 102 const ZERO_DIGIT_SYMBOL = 4; 103 const DIGIT_SYMBOL = 5; 104 const MINUS_SIGN_SYMBOL = 6; 105 const PLUS_SIGN_SYMBOL = 7; 106 const CURRENCY_SYMBOL = 8; 107 const INTL_CURRENCY_SYMBOL = 9; 108 const MONETARY_SEPARATOR_SYMBOL = 10; 109 const EXPONENTIAL_SYMBOL = 11; 110 const PERMILL_SYMBOL = 12; 111 const PAD_ESCAPE_SYMBOL = 13; 112 const INFINITY_SYMBOL = 14; 113 const NAN_SYMBOL = 15; 114 const SIGNIFICANT_DIGIT_SYMBOL = 16; 115 const MONETARY_GROUPING_SEPARATOR_SYMBOL = 17; 116 117 /* Rounding mode values used by NumberFormatter::setAttribute() with NumberFormatter::ROUNDING_MODE attribute */ 118 const ROUND_CEILING = 0; 119 const ROUND_FLOOR = 1; 120 const ROUND_DOWN = 2; 121 const ROUND_UP = 3; 122 const ROUND_HALFEVEN = 4; 123 const ROUND_HALFDOWN = 5; 124 const ROUND_HALFUP = 6; 125 126 /* Pad position values used by NumberFormatter::setAttribute() with NumberFormatter::PADDING_POSITION attribute */ 127 const PAD_BEFORE_PREFIX = 0; 128 const PAD_AFTER_PREFIX = 1; 129 const PAD_BEFORE_SUFFIX = 2; 130 const PAD_AFTER_SUFFIX = 3; 131 132 /** 133 * The error code from the last operation. 134 * 135 * @var int 136 */ 137 protected $errorCode = IntlGlobals::U_ZERO_ERROR; 138 139 /** 140 * The error message from the last operation. 141 * 142 * @var string 143 */ 144 protected $errorMessage = 'U_ZERO_ERROR'; 145 146 /** 147 * @var int 148 */ 149 private $style; 150 151 /** 152 * Default values for the en locale. 153 */ 154 private $attributes = [ 155 self::FRACTION_DIGITS => 0, 156 self::GROUPING_USED => 1, 157 self::ROUNDING_MODE => self::ROUND_HALFEVEN, 158 ]; 159 160 /** 161 * Holds the initialized attributes code. 162 */ 163 private $initializedAttributes = []; 164 165 /** 166 * The supported styles to the constructor $styles argument. 167 */ 168 private static $supportedStyles = [ 169 'CURRENCY' => self::CURRENCY, 170 'DECIMAL' => self::DECIMAL, 171 ]; 172 173 /** 174 * Supported attributes to the setAttribute() $attr argument. 175 */ 176 private static $supportedAttributes = [ 177 'FRACTION_DIGITS' => self::FRACTION_DIGITS, 178 'GROUPING_USED' => self::GROUPING_USED, 179 'ROUNDING_MODE' => self::ROUNDING_MODE, 180 ]; 181 182 /** 183 * The available rounding modes for setAttribute() usage with 184 * NumberFormatter::ROUNDING_MODE. NumberFormatter::ROUND_DOWN 185 * and NumberFormatter::ROUND_UP does not have a PHP only equivalent. 186 */ 187 private static $roundingModes = [ 188 'ROUND_HALFEVEN' => self::ROUND_HALFEVEN, 189 'ROUND_HALFDOWN' => self::ROUND_HALFDOWN, 190 'ROUND_HALFUP' => self::ROUND_HALFUP, 191 'ROUND_CEILING' => self::ROUND_CEILING, 192 'ROUND_FLOOR' => self::ROUND_FLOOR, 193 'ROUND_DOWN' => self::ROUND_DOWN, 194 'ROUND_UP' => self::ROUND_UP, 195 ]; 196 197 /** 198 * The mapping between NumberFormatter rounding modes to the available 199 * modes in PHP's round() function. 200 * 201 * @see https://php.net/round 202 */ 203 private static $phpRoundingMap = [ 204 self::ROUND_HALFDOWN => \PHP_ROUND_HALF_DOWN, 205 self::ROUND_HALFEVEN => \PHP_ROUND_HALF_EVEN, 206 self::ROUND_HALFUP => \PHP_ROUND_HALF_UP, 207 ]; 208 209 /** 210 * The list of supported rounding modes which aren't available modes in 211 * PHP's round() function, but there's an equivalent. Keys are rounding 212 * modes, values does not matter. 213 */ 214 private static $customRoundingList = [ 215 self::ROUND_CEILING => true, 216 self::ROUND_FLOOR => true, 217 self::ROUND_DOWN => true, 218 self::ROUND_UP => true, 219 ]; 220 221 /** 222 * The maximum value of the integer type in 32 bit platforms. 223 */ 224 private static $int32Max = 2147483647; 225 226 /** 227 * The maximum value of the integer type in 64 bit platforms. 228 * 229 * @var int|float 230 */ 231 private static $int64Max = 9223372036854775807; 232 233 private static $enSymbols = [ 234 self::DECIMAL => ['.', ',', ';', '%', '0', '#', '-', '+', '¤', '¤¤', '.', 'E', '‰', '*', '∞', 'NaN', '@', ','], 235 self::CURRENCY => ['.', ',', ';', '%', '0', '#', '-', '+', '¤', '¤¤', '.', 'E', '‰', '*', '∞', 'NaN', '@', ','], 236 ]; 237 238 private static $enTextAttributes = [ 239 self::DECIMAL => ['', '', '-', '', ' ', 'XXX', ''], 240 self::CURRENCY => ['¤', '', '-¤', '', ' ', 'XXX'], 241 ]; 242 243 /** 244 * @param string|null $locale The locale code. The only currently supported locale is "en" (or null using the default locale, i.e. "en") 245 * @param int $style Style of the formatting, one of the format style constants. 246 * The only supported styles are NumberFormatter::DECIMAL 247 * and NumberFormatter::CURRENCY. 248 * @param string $pattern Not supported. A pattern string in case $style is NumberFormat::PATTERN_DECIMAL or 249 * NumberFormat::PATTERN_RULEBASED. It must conform to the syntax 250 * described in the ICU DecimalFormat or ICU RuleBasedNumberFormat documentation 251 * 252 * @see https://php.net/numberformatter.create 253 * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details 254 * @see http://www.icu-project.org/apiref/icu4c/classRuleBasedNumberFormat.html#_details 255 * 256 * @throws MethodArgumentValueNotImplementedException When $locale different than "en" or null is passed 257 * @throws MethodArgumentValueNotImplementedException When the $style is not supported 258 * @throws MethodArgumentNotImplementedException When the pattern value is different than null 259 */ 260 public function __construct($locale = 'en', $style = null, $pattern = null) 261 { 262 if ('en' !== $locale && null !== $locale) { 263 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'locale', $locale, 'Only the locale "en" is supported'); 264 } 265 266 if (!\in_array($style, self::$supportedStyles)) { 267 $message = sprintf('The available styles are: %s.', implode(', ', array_keys(self::$supportedStyles))); 268 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'style', $style, $message); 269 } 270 271 if (null !== $pattern) { 272 throw new MethodArgumentNotImplementedException(__METHOD__, 'pattern'); 273 } 274 275 $this->style = null !== $style ? (int) $style : null; 276 } 277 278 /** 279 * Static constructor. 280 * 281 * @param string|null $locale The locale code. The only supported locale is "en" (or null using the default locale, i.e. "en") 282 * @param int $style Style of the formatting, one of the format style constants. 283 * The only currently supported styles are NumberFormatter::DECIMAL 284 * and NumberFormatter::CURRENCY. 285 * @param string $pattern Not supported. A pattern string in case $style is NumberFormat::PATTERN_DECIMAL or 286 * NumberFormat::PATTERN_RULEBASED. It must conform to the syntax 287 * described in the ICU DecimalFormat or ICU RuleBasedNumberFormat documentation 288 * 289 * @return self 290 * 291 * @see https://php.net/numberformatter.create 292 * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details 293 * @see http://www.icu-project.org/apiref/icu4c/classRuleBasedNumberFormat.html#_details 294 * 295 * @throws MethodArgumentValueNotImplementedException When $locale different than "en" or null is passed 296 * @throws MethodArgumentValueNotImplementedException When the $style is not supported 297 * @throws MethodArgumentNotImplementedException When the pattern value is different than null 298 */ 299 public static function create($locale = 'en', $style = null, $pattern = null) 300 { 301 return new self($locale, $style, $pattern); 302 } 303 304 /** 305 * Format a currency value. 306 * 307 * @param float $value The numeric currency value 308 * @param string $currency The 3-letter ISO 4217 currency code indicating the currency to use 309 * 310 * @return string The formatted currency value 311 * 312 * @see https://php.net/numberformatter.formatcurrency 313 * @see https://en.wikipedia.org/wiki/ISO_4217#Active_codes 314 */ 315 public function formatCurrency($value, $currency) 316 { 317 if (self::DECIMAL === $this->style) { 318 return $this->format($value); 319 } 320 321 $symbol = Intl::getCurrencyBundle()->getCurrencySymbol($currency, 'en'); 322 $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency); 323 324 $value = $this->roundCurrency($value, $currency); 325 326 $negative = false; 327 if (0 > $value) { 328 $negative = true; 329 $value *= -1; 330 } 331 332 $value = $this->formatNumber($value, $fractionDigits); 333 334 // There's a non-breaking space after the currency code (i.e. CRC 100), but not if the currency has a symbol (i.e. £100). 335 $ret = $symbol.(mb_strlen($symbol, 'UTF-8') > 2 ? "\xc2\xa0" : '').$value; 336 337 return $negative ? '-'.$ret : $ret; 338 } 339 340 /** 341 * Format a number. 342 * 343 * @param int|float $value The value to format 344 * @param int $type Type of the formatting, one of the format type constants. 345 * Only type NumberFormatter::TYPE_DEFAULT is currently supported. 346 * 347 * @return bool|string The formatted value or false on error 348 * 349 * @see https://php.net/numberformatter.format 350 * 351 * @throws NotImplementedException If the method is called with the class $style 'CURRENCY' 352 * @throws MethodArgumentValueNotImplementedException If the $type is different than TYPE_DEFAULT 353 */ 354 public function format($value, $type = self::TYPE_DEFAULT) 355 { 356 $type = (int) $type; 357 358 // The original NumberFormatter does not support this format type 359 if (self::TYPE_CURRENCY === $type) { 360 if (\PHP_VERSION_ID >= 80000) { 361 throw new \ValueError(sprintf('The format type must be a NumberFormatter::TYPE_* constant (%s given).', $type)); 362 } 363 364 trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING); 365 366 return false; 367 } 368 369 if (self::CURRENCY === $this->style) { 370 throw new NotImplementedException(sprintf('"%s()" method does not support the formatting of currencies (instance with CURRENCY style). "%s".', __METHOD__, NotImplementedException::INTL_INSTALL_MESSAGE)); 371 } 372 373 // Only the default type is supported. 374 if (self::TYPE_DEFAULT !== $type) { 375 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'type', $type, 'Only TYPE_DEFAULT is supported'); 376 } 377 378 $fractionDigits = $this->getAttribute(self::FRACTION_DIGITS); 379 380 $value = $this->round($value, $fractionDigits); 381 $value = $this->formatNumber($value, $fractionDigits); 382 383 // behave like the intl extension 384 $this->resetError(); 385 386 return $value; 387 } 388 389 /** 390 * Returns an attribute value. 391 * 392 * @param int $attr An attribute specifier, one of the numeric attribute constants 393 * 394 * @return int|false The attribute value on success or false on error 395 * 396 * @see https://php.net/numberformatter.getattribute 397 */ 398 public function getAttribute($attr) 399 { 400 return isset($this->attributes[$attr]) ? $this->attributes[$attr] : null; 401 } 402 403 /** 404 * Returns formatter's last error code. Always returns the U_ZERO_ERROR class constant value. 405 * 406 * @return int The error code from last formatter call 407 * 408 * @see https://php.net/numberformatter.geterrorcode 409 */ 410 public function getErrorCode() 411 { 412 return $this->errorCode; 413 } 414 415 /** 416 * Returns formatter's last error message. Always returns the U_ZERO_ERROR_MESSAGE class constant value. 417 * 418 * @return string The error message from last formatter call 419 * 420 * @see https://php.net/numberformatter.geterrormessage 421 */ 422 public function getErrorMessage() 423 { 424 return $this->errorMessage; 425 } 426 427 /** 428 * Returns the formatter's locale. 429 * 430 * The parameter $type is currently ignored. 431 * 432 * @param int $type Not supported. The locale name type to return (Locale::VALID_LOCALE or Locale::ACTUAL_LOCALE) 433 * 434 * @return string The locale used to create the formatter. Currently always 435 * returns "en". 436 * 437 * @see https://php.net/numberformatter.getlocale 438 */ 439 public function getLocale($type = Locale::ACTUAL_LOCALE) 440 { 441 return 'en'; 442 } 443 444 /** 445 * Not supported. Returns the formatter's pattern. 446 * 447 * @return string|false The pattern string used by the formatter or false on error 448 * 449 * @see https://php.net/numberformatter.getpattern 450 * 451 * @throws MethodNotImplementedException 452 */ 453 public function getPattern() 454 { 455 throw new MethodNotImplementedException(__METHOD__); 456 } 457 458 /** 459 * Not supported. Returns a formatter symbol value. 460 * 461 * @param int $attr A symbol specifier, one of the format symbol constants 462 * 463 * @return string|false The symbol value or false on error 464 * 465 * @see https://php.net/numberformatter.getsymbol 466 */ 467 public function getSymbol($attr) 468 { 469 return \array_key_exists($this->style, self::$enSymbols) && \array_key_exists($attr, self::$enSymbols[$this->style]) ? self::$enSymbols[$this->style][$attr] : false; 470 } 471 472 /** 473 * Not supported. Returns a formatter text attribute value. 474 * 475 * @param int $attr An attribute specifier, one of the text attribute constants 476 * 477 * @return string|false The attribute value or false on error 478 * 479 * @see https://php.net/numberformatter.gettextattribute 480 */ 481 public function getTextAttribute($attr) 482 { 483 return \array_key_exists($this->style, self::$enTextAttributes) && \array_key_exists($attr, self::$enTextAttributes[$this->style]) ? self::$enTextAttributes[$this->style][$attr] : false; 484 } 485 486 /** 487 * Not supported. Parse a currency number. 488 * 489 * @param string $value The value to parse 490 * @param string $currency Parameter to receive the currency name (reference) 491 * @param int $position Offset to begin the parsing on return this value will hold the offset at which the parsing ended 492 * 493 * @return float|false The parsed numeric value or false on error 494 * 495 * @see https://php.net/numberformatter.parsecurrency 496 * 497 * @throws MethodNotImplementedException 498 */ 499 public function parseCurrency($value, &$currency, &$position = null) 500 { 501 throw new MethodNotImplementedException(__METHOD__); 502 } 503 504 /** 505 * Parse a number. 506 * 507 * @param string $value The value to parse 508 * @param int $type Type of the formatting, one of the format type constants. NumberFormatter::TYPE_DOUBLE by default. 509 * @param int $position Offset to begin the parsing on return this value will hold the offset at which the parsing ended 510 * 511 * @return int|float|false The parsed value or false on error 512 * 513 * @see https://php.net/numberformatter.parse 514 */ 515 public function parse($value, $type = self::TYPE_DOUBLE, &$position = 0) 516 { 517 $type = (int) $type; 518 519 if (self::TYPE_DEFAULT === $type || self::TYPE_CURRENCY === $type) { 520 if (\PHP_VERSION_ID >= 80000) { 521 throw new \ValueError(sprintf('The format type must be a NumberFormatter::TYPE_* constant (%d given).', $type)); 522 } 523 524 trigger_error(__METHOD__.'(): Unsupported format type '.$type, \E_USER_WARNING); 525 526 return false; 527 } 528 529 // Any invalid number at the end of the string is removed. 530 // Only numbers and the fraction separator is expected in the string. 531 // If grouping is used, grouping separator also becomes a valid character. 532 $groupingMatch = $this->getAttribute(self::GROUPING_USED) ? '|(?P<grouping>\d++(,{1}\d+)++(\.\d*+)?)' : ''; 533 if (preg_match("/^-?(?:\.\d++{$groupingMatch}|\d++(\.\d*+)?)/", $value, $matches)) { 534 $value = $matches[0]; 535 $position = \strlen($value); 536 // value is not valid if grouping is used, but digits are not grouped in groups of three 537 if ($error = isset($matches['grouping']) && !preg_match('/^-?(?:\d{1,3}+)?(?:(?:,\d{3})++|\d*+)(?:\.\d*+)?$/', $value)) { 538 // the position on error is 0 for positive and 1 for negative numbers 539 $position = 0 === strpos($value, '-') ? 1 : 0; 540 } 541 } else { 542 $error = true; 543 $position = 0; 544 } 545 546 if ($error) { 547 IntlGlobals::setError(IntlGlobals::U_PARSE_ERROR, 'Number parsing failed'); 548 $this->errorCode = IntlGlobals::getErrorCode(); 549 $this->errorMessage = IntlGlobals::getErrorMessage(); 550 551 return false; 552 } 553 554 $value = str_replace(',', '', $value); 555 $value = $this->convertValueDataType($value, $type); 556 557 // behave like the intl extension 558 $this->resetError(); 559 560 return $value; 561 } 562 563 /** 564 * Set an attribute. 565 * 566 * @param int $attr An attribute specifier, one of the numeric attribute constants. 567 * The only currently supported attributes are NumberFormatter::FRACTION_DIGITS, 568 * NumberFormatter::GROUPING_USED and NumberFormatter::ROUNDING_MODE. 569 * @param int $value The attribute value 570 * 571 * @return bool true on success or false on failure 572 * 573 * @see https://php.net/numberformatter.setattribute 574 * 575 * @throws MethodArgumentValueNotImplementedException When the $attr is not supported 576 * @throws MethodArgumentValueNotImplementedException When the $value is not supported 577 */ 578 public function setAttribute($attr, $value) 579 { 580 $attr = (int) $attr; 581 582 if (!\in_array($attr, self::$supportedAttributes)) { 583 $message = sprintf( 584 'The available attributes are: %s', 585 implode(', ', array_keys(self::$supportedAttributes)) 586 ); 587 588 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'attr', $value, $message); 589 } 590 591 if (self::$supportedAttributes['ROUNDING_MODE'] === $attr && $this->isInvalidRoundingMode($value)) { 592 $message = sprintf( 593 'The supported values for ROUNDING_MODE are: %s', 594 implode(', ', array_keys(self::$roundingModes)) 595 ); 596 597 throw new MethodArgumentValueNotImplementedException(__METHOD__, 'attr', $value, $message); 598 } 599 600 if (self::$supportedAttributes['GROUPING_USED'] === $attr) { 601 $value = $this->normalizeGroupingUsedValue($value); 602 } 603 604 if (self::$supportedAttributes['FRACTION_DIGITS'] === $attr) { 605 $value = $this->normalizeFractionDigitsValue($value); 606 if ($value < 0) { 607 // ignore negative values but do not raise an error 608 return true; 609 } 610 } 611 612 $this->attributes[$attr] = $value; 613 $this->initializedAttributes[$attr] = true; 614 615 return true; 616 } 617 618 /** 619 * Not supported. Set the formatter's pattern. 620 * 621 * @param string $pattern A pattern string in conformance with the ICU DecimalFormat documentation 622 * 623 * @return bool true on success or false on failure 624 * 625 * @see https://php.net/numberformatter.setpattern 626 * @see http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details 627 * 628 * @throws MethodNotImplementedException 629 */ 630 public function setPattern($pattern) 631 { 632 throw new MethodNotImplementedException(__METHOD__); 633 } 634 635 /** 636 * Not supported. Set the formatter's symbol. 637 * 638 * @param int $attr A symbol specifier, one of the format symbol constants 639 * @param string $value The value for the symbol 640 * 641 * @return bool true on success or false on failure 642 * 643 * @see https://php.net/numberformatter.setsymbol 644 * 645 * @throws MethodNotImplementedException 646 */ 647 public function setSymbol($attr, $value) 648 { 649 throw new MethodNotImplementedException(__METHOD__); 650 } 651 652 /** 653 * Not supported. Set a text attribute. 654 * 655 * @param int $attr An attribute specifier, one of the text attribute constants 656 * @param string $value The attribute value 657 * 658 * @return bool true on success or false on failure 659 * 660 * @see https://php.net/numberformatter.settextattribute 661 * 662 * @throws MethodNotImplementedException 663 */ 664 public function setTextAttribute($attr, $value) 665 { 666 throw new MethodNotImplementedException(__METHOD__); 667 } 668 669 /** 670 * Set the error to the default U_ZERO_ERROR. 671 */ 672 protected function resetError() 673 { 674 IntlGlobals::setError(IntlGlobals::U_ZERO_ERROR); 675 $this->errorCode = IntlGlobals::getErrorCode(); 676 $this->errorMessage = IntlGlobals::getErrorMessage(); 677 } 678 679 /** 680 * Rounds a currency value, applying increment rounding if applicable. 681 * 682 * When a currency have a rounding increment, an extra round is made after the first one. The rounding factor is 683 * determined in the ICU data and is explained as of: 684 * 685 * "the rounding increment is given in units of 10^(-fraction_digits)" 686 * 687 * The only actual rounding data as of this writing, is CHF. 688 * 689 * @param float $value The numeric currency value 690 * @param string $currency The 3-letter ISO 4217 currency code indicating the currency to use 691 * 692 * @return float The rounded numeric currency value 693 * 694 * @see http://en.wikipedia.org/wiki/Swedish_rounding 695 * @see http://www.docjar.com/html/api/com/ibm/icu/util/Currency.java.html#1007 696 */ 697 private function roundCurrency($value, $currency) 698 { 699 $fractionDigits = Intl::getCurrencyBundle()->getFractionDigits($currency); 700 $roundingIncrement = Intl::getCurrencyBundle()->getRoundingIncrement($currency); 701 702 // Round with the formatter rounding mode 703 $value = $this->round($value, $fractionDigits); 704 705 // Swiss rounding 706 if (0 < $roundingIncrement && 0 < $fractionDigits) { 707 $roundingFactor = $roundingIncrement / pow(10, $fractionDigits); 708 $value = round($value / $roundingFactor) * $roundingFactor; 709 } 710 711 return $value; 712 } 713 714 /** 715 * Rounds a value. 716 * 717 * @param int|float $value The value to round 718 * @param int $precision The number of decimal digits to round to 719 * 720 * @return int|float The rounded value 721 */ 722 private function round($value, $precision) 723 { 724 $precision = $this->getUninitializedPrecision($value, $precision); 725 726 $roundingModeAttribute = $this->getAttribute(self::ROUNDING_MODE); 727 if (isset(self::$phpRoundingMap[$roundingModeAttribute])) { 728 $value = round($value, $precision, self::$phpRoundingMap[$roundingModeAttribute]); 729 } elseif (isset(self::$customRoundingList[$roundingModeAttribute])) { 730 $roundingCoef = pow(10, $precision); 731 $value *= $roundingCoef; 732 $value = (float) (string) $value; 733 734 switch ($roundingModeAttribute) { 735 case self::ROUND_CEILING: 736 $value = ceil($value); 737 break; 738 case self::ROUND_FLOOR: 739 $value = floor($value); 740 break; 741 case self::ROUND_UP: 742 $value = $value > 0 ? ceil($value) : floor($value); 743 break; 744 case self::ROUND_DOWN: 745 $value = $value > 0 ? floor($value) : ceil($value); 746 break; 747 } 748 749 $value /= $roundingCoef; 750 } 751 752 return $value; 753 } 754 755 /** 756 * Formats a number. 757 * 758 * @param int|float $value The numeric value to format 759 * @param int $precision The number of decimal digits to use 760 * 761 * @return string The formatted number 762 */ 763 private function formatNumber($value, $precision) 764 { 765 $precision = $this->getUninitializedPrecision($value, $precision); 766 767 return number_format($value, $precision, '.', $this->getAttribute(self::GROUPING_USED) ? ',' : ''); 768 } 769 770 /** 771 * Returns the precision value if the DECIMAL style is being used and the FRACTION_DIGITS attribute is uninitialized. 772 * 773 * @param int|float $value The value to get the precision from if the FRACTION_DIGITS attribute is uninitialized 774 * @param int $precision The precision value to returns if the FRACTION_DIGITS attribute is initialized 775 * 776 * @return int The precision value 777 */ 778 private function getUninitializedPrecision($value, $precision) 779 { 780 if (self::CURRENCY === $this->style) { 781 return $precision; 782 } 783 784 if (!$this->isInitializedAttribute(self::FRACTION_DIGITS)) { 785 preg_match('/.*\.(.*)/', (string) $value, $digits); 786 if (isset($digits[1])) { 787 $precision = \strlen($digits[1]); 788 } 789 } 790 791 return $precision; 792 } 793 794 /** 795 * Check if the attribute is initialized (value set by client code). 796 * 797 * @param string $attr The attribute name 798 * 799 * @return bool true if the value was set by client, false otherwise 800 */ 801 private function isInitializedAttribute($attr) 802 { 803 return isset($this->initializedAttributes[$attr]); 804 } 805 806 /** 807 * Returns the numeric value using the $type to convert to the right data type. 808 * 809 * @param mixed $value The value to be converted 810 * @param int $type The type to convert. Can be TYPE_DOUBLE (float) or TYPE_INT32 (int) 811 * 812 * @return int|float|false The converted value 813 */ 814 private function convertValueDataType($value, $type) 815 { 816 $type = (int) $type; 817 818 if (self::TYPE_DOUBLE === $type) { 819 $value = (float) $value; 820 } elseif (self::TYPE_INT32 === $type) { 821 $value = $this->getInt32Value($value); 822 } elseif (self::TYPE_INT64 === $type) { 823 $value = $this->getInt64Value($value); 824 } 825 826 return $value; 827 } 828 829 /** 830 * Convert the value data type to int or returns false if the value is out of the integer value range. 831 * 832 * @param mixed $value The value to be converted 833 * 834 * @return int|false The converted value 835 */ 836 private function getInt32Value($value) 837 { 838 if ($value > self::$int32Max || $value < -self::$int32Max - 1) { 839 return false; 840 } 841 842 return (int) $value; 843 } 844 845 /** 846 * Convert the value data type to int or returns false if the value is out of the integer value range. 847 * 848 * @param mixed $value The value to be converted 849 * 850 * @return int|float|false The converted value 851 */ 852 private function getInt64Value($value) 853 { 854 if ($value > self::$int64Max || $value < -self::$int64Max - 1) { 855 return false; 856 } 857 858 if (\PHP_INT_SIZE !== 8 && ($value > self::$int32Max || $value < -self::$int32Max - 1)) { 859 return (float) $value; 860 } 861 862 return (int) $value; 863 } 864 865 /** 866 * Check if the rounding mode is invalid. 867 * 868 * @param int $value The rounding mode value to check 869 * 870 * @return bool true if the rounding mode is invalid, false otherwise 871 */ 872 private function isInvalidRoundingMode($value) 873 { 874 if (\in_array($value, self::$roundingModes, true)) { 875 return false; 876 } 877 878 return true; 879 } 880 881 /** 882 * Returns the normalized value for the GROUPING_USED attribute. Any value that can be converted to int will be 883 * cast to Boolean and then to int again. This way, negative values are converted to 1 and string values to 0. 884 * 885 * @param mixed $value The value to be normalized 886 * 887 * @return int The normalized value for the attribute (0 or 1) 888 */ 889 private function normalizeGroupingUsedValue($value) 890 { 891 return (int) (bool) (int) $value; 892 } 893 894 /** 895 * Returns the normalized value for the FRACTION_DIGITS attribute. 896 * 897 * @param mixed $value The value to be normalized 898 * 899 * @return int The normalized value for the attribute 900 */ 901 private function normalizeFractionDigitsValue($value) 902 { 903 return (int) $value; 904 } 905} 906