diff options
author | Mario <mario@mariovavti.com> | 2019-11-10 12:49:51 +0000 |
---|---|---|
committer | Mario <mario@mariovavti.com> | 2019-11-10 14:10:03 +0100 |
commit | 580c3f4ffe9608d2beb56d418c68b3b112420e76 (patch) | |
tree | 82335d01179ac361d3f547a4b8e8c598d302e9f3 /vendor/commerceguys/intl/src/Formatter | |
parent | d22766f458a8539a40a57f3946459a9be1f21cd6 (diff) | |
download | volse-hubzilla-580c3f4ffe9608d2beb56d418c68b3b112420e76.tar.gz volse-hubzilla-580c3f4ffe9608d2beb56d418c68b3b112420e76.tar.bz2 volse-hubzilla-580c3f4ffe9608d2beb56d418c68b3b112420e76.zip |
another bulk of composer updates
(cherry picked from commit 6685381fd8db507493c3d7c1793f8c05c681bbce)
Diffstat (limited to 'vendor/commerceguys/intl/src/Formatter')
6 files changed, 749 insertions, 470 deletions
diff --git a/vendor/commerceguys/intl/src/Formatter/CurrencyFormatter.php b/vendor/commerceguys/intl/src/Formatter/CurrencyFormatter.php new file mode 100644 index 000000000..8d4d11f27 --- /dev/null +++ b/vendor/commerceguys/intl/src/Formatter/CurrencyFormatter.php @@ -0,0 +1,241 @@ +<?php + +namespace CommerceGuys\Intl\Formatter; + +use CommerceGuys\Intl\Currency\Currency; +use CommerceGuys\Intl\Currency\CurrencyRepositoryInterface; +use CommerceGuys\Intl\Exception\InvalidArgumentException; +use CommerceGuys\Intl\Exception\UnknownCurrencyException; +use CommerceGuys\Intl\NumberFormat\NumberFormat; +use CommerceGuys\Intl\NumberFormat\NumberFormatRepositoryInterface; + +/** + * Formats currency amounts using locale-specific patterns. + */ +class CurrencyFormatter implements CurrencyFormatterInterface +{ + use FormatterTrait; + + /** + * The number format repository. + * + * @var NumberFormatRepositoryInterface + */ + protected $numberFormatRepository; + + /** + * The currency repository. + * + * @var CurrencyRepositoryInterface + */ + protected $currencyRepository; + + /** + * The default locale. + * + * @var string + */ + protected $defaultLocale; + + /** + * The loaded number formats. + * + * @var NumberFormat[] + */ + protected $numberFormats = []; + + /** + * The loaded currencies. + * + * @var Currency[] + */ + protected $currencies = []; + + /** + * The default options. + * + * @var array + */ + protected $defaultOptions = [ + 'locale' => 'en', + 'use_grouping' => true, + 'minimum_fraction_digits' => null, + 'maximum_fraction_digits' => null, + 'rounding_mode' => PHP_ROUND_HALF_UP, + 'style' => 'standard', + 'currency_display' => 'symbol', + ]; + + /** + * Creates a CurrencyFormatter instance. + * + * @param NumberFormatRepositoryInterface $numberFormatRepository The number format repository. + * @param CurrencyRepositoryInterface $currencyRepository The currency repository. + * @param array $defaultOptions The default options. + * + * @throws \RuntimeException + */ + public function __construct(NumberFormatRepositoryInterface $numberFormatRepository, CurrencyRepositoryInterface $currencyRepository, array $defaultOptions = []) + { + if (!extension_loaded('bcmath')) { + throw new \RuntimeException('The bcmath extension is required by CurrencyFormatter.'); + } + $this->validateOptions($defaultOptions); + + $this->numberFormatRepository = $numberFormatRepository; + $this->currencyRepository = $currencyRepository; + $this->defaultOptions = array_replace($this->defaultOptions, $defaultOptions); + } + + /** + * {@inheritdoc} + */ + public function format($number, $currencyCode, array $options = []) + { + if (!is_numeric($number)) { + $message = sprintf('The provided value "%s" is not a valid number or numeric string.', $number); + throw new InvalidArgumentException($message); + } + + $this->validateOptions($options); + $options = array_replace($this->defaultOptions, $options); + $numberFormat = $this->getNumberFormat($options['locale']); + $currency = $this->getCurrency($currencyCode, $options['locale']); + // Use the currency defaults if the values weren't set by the caller. + if (!isset($options['minimum_fraction_digits'])) { + $options['minimum_fraction_digits'] = $currency->getFractionDigits(); + } + if (!isset($options['maximum_fraction_digits'])) { + $options['maximum_fraction_digits'] = $currency->getFractionDigits(); + } + + $number = (string) $number; + $number = $this->formatNumber($number, $numberFormat, $options); + if ($options['currency_display'] == 'symbol') { + $number = str_replace('¤', $currency->getSymbol(), $number); + } elseif ($options['currency_display'] == 'code') { + $number = str_replace('¤', $currency->getCurrencyCode(), $number); + } else { + // No symbol should be displayed. Remove leftover whitespace. + $number = str_replace('¤', '', $number); + $number = trim($number, " \xC2\xA0"); + } + + return $number; + } + + /** + * {@inheritdoc} + */ + public function parse($number, $currencyCode, array $options = []) + { + $this->validateOptions($options); + $options = array_replace($this->defaultOptions, $options); + $numberFormat = $this->getNumberFormat($options['locale']); + $currency = $this->getCurrency($currencyCode, $options['locale']); + $replacements = [ + // Strip the currency code or symbol. + $currency->getCurrencyCode() => '', + $currency->getSymbol() => '', + ]; + $number = strtr($number, $replacements); + $number = $this->parseNumber($number, $numberFormat); + + return $number; + } + + /** + * Gets the number format for the provided locale. + * + * @param string $locale The locale. + * + * @return NumberFormat + */ + protected function getNumberFormat($locale) + { + if (!isset($this->numberFormats[$locale])) { + $this->numberFormats[$locale] = $this->numberFormatRepository->get($locale); + } + + return $this->numberFormats[$locale]; + } + + /** + * Gets the currency for the provided currency code and locale. + * + * @param string $currencyCode The currency code. + * @param string $locale The locale. + * + * @return Currency + */ + protected function getCurrency($currencyCode, $locale) + { + if (!isset($this->currencies[$currencyCode][$locale])) { + try { + $currency = $this->currencyRepository->get($currencyCode, $locale); + } catch (UnknownCurrencyException $e) { + // The requested currency was not found. Fall back + // to a dummy object to show just the currency code. + $currency = new Currency([ + 'currency_code' => $currencyCode, + 'name' => $currencyCode, + 'numeric_code' => '000', + 'locale' => $locale, + ]); + } + $this->currencies[$currencyCode][$locale] = $currency; + } + + return $this->currencies[$currencyCode][$locale]; + } + + /** + * {@inheritdoc} + */ + protected function getAvailablePatterns(NumberFormat $numberFormat) + { + return [ + 'standard' => $numberFormat->getCurrencyPattern(), + 'accounting' => $numberFormat->getAccountingCurrencyPattern(), + ]; + } + + /** + * Validates the provided options. + * + * Ensures the absence of unknown keys, correct data types and values. + * + * @param array $options The options. + * + * @throws \InvalidArgumentException + */ + protected function validateOptions(array $options) + { + foreach ($options as $option => $value) { + if (!array_key_exists($option, $this->defaultOptions)) { + throw new InvalidArgumentException(sprintf('Unrecognized option "%s".', $option)); + } + } + if (isset($options['use_grouping']) && !is_bool($options['use_grouping'])) { + throw new InvalidArgumentException('The option "use_grouping" must be a boolean.'); + } + foreach (['minimum_fraction_digits', 'maximum_fraction_digits'] as $option) { + if (array_key_exists($option, $options) && !is_numeric($options[$option])) { + throw new InvalidArgumentException(sprintf('The option "%s" must be numeric.', $option)); + } + } + $roundingModes = [ + PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, + PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD, 'none', + ]; + if (!empty($options['rounding_mode']) && !in_array($options['rounding_mode'], $roundingModes)) { + throw new InvalidArgumentException(sprintf('Unrecognized rounding mode "%s".', $options['rounding_mode'])); + } + if (!empty($options['style']) && !in_array($options['style'], ['standard', 'accounting'])) { + throw new InvalidArgumentException(sprintf('Unrecognized style "%s".', $options['style'])); + } + if (!empty($options['currency_display']) && !in_array($options['currency_display'], ['code', 'symbol', 'none'])) { + throw new InvalidArgumentException(sprintf('Unrecognized currency display "%s".', $options['currency_display'])); + } + } +} diff --git a/vendor/commerceguys/intl/src/Formatter/CurrencyFormatterInterface.php b/vendor/commerceguys/intl/src/Formatter/CurrencyFormatterInterface.php new file mode 100644 index 000000000..3fc25fcc6 --- /dev/null +++ b/vendor/commerceguys/intl/src/Formatter/CurrencyFormatterInterface.php @@ -0,0 +1,54 @@ +<?php + +namespace CommerceGuys\Intl\Formatter; + +interface CurrencyFormatterInterface +{ + /** + * Formats a currency amount. + * + * Supported options: + * - locale: The locale. Default: 'en'. + * - use_grouping: Whether to use grouping separators, + * such as thousands separators. + * Default: true. + * - minimum_fraction_digits: Minimum fraction digits. + * - maximum_fraction_digits: Minimum fraction digits. + * - rounding_mode: The rounding mode. + * A PHP_ROUND_ constant or 'none' to skip + * rounding. Default: PHP_ROUND_HALF_UP. + * - style: The style. + * One of: 'standard', 'accounting'. + * Default: 'standard'. + * - currency_display: How the currency should be displayed. + * One of: 'code', 'symbol', 'none'. + * Default: 'symbol'. + * + * Both minimum_fraction_digits and maximum_fraction_digits default + * to the currency's number of fraction digits. + * + * @param string $number The number. + * @param string $currencyCode The currency code. + * @param array $options The formatting options. + * + * @return string The formatted number. + */ + public function format($number, $currencyCode, array $options = []); + + /** + * Parses a formatted currency amount. + * + * Commonly used in input widgets where the end-user might input + * a number using digits and symbols common to their locale. + * + * Supported options: + * - locale: The locale. Default: 'en'. + * + * @param string $number The formatted number. + * @param string $currencyCode The currency code. + * @param array $options The parsing options. + * + * @return string|false The parsed number or FALSE on error. + */ + public function parse($number, $currencyCode, array $options = []); +} diff --git a/vendor/commerceguys/intl/src/Formatter/FormatterTrait.php b/vendor/commerceguys/intl/src/Formatter/FormatterTrait.php new file mode 100644 index 000000000..aa63c1f5f --- /dev/null +++ b/vendor/commerceguys/intl/src/Formatter/FormatterTrait.php @@ -0,0 +1,215 @@ +<?php + +namespace CommerceGuys\Intl\Formatter; + +use CommerceGuys\Intl\Calculator; +use CommerceGuys\Intl\Exception\InvalidArgumentException; +use CommerceGuys\Intl\NumberFormat\NumberFormat; + +trait FormatterTrait +{ + /** + * The parsed number patterns, keyed by locale and style. + * + * @var ParsedPattern[] + */ + protected $parsedPatterns = []; + + /** + * Localized digits. + * + * @var array + */ + protected $digits = [ + NumberFormat::NUMBERING_SYSTEM_ARABIC => [ + 0 => '٠', 1 => '١', 2 => '٢', 3 => '٣', 4 => '٤', + 5 => '٥', 6 => '٦', 7 => '٧', 8 => '٨', 9 => '٩', + ], + NumberFormat::NUMBERING_SYSTEM_ARABIC_EXTENDED => [ + 0 => '۰', 1 => '۱', 2 => '۲', 3 => '۳', 4 => '۴', + 5 => '۵', 6 => '۶', 7 => '۷', 8 => '۸', 9 => '۹', + ], + NumberFormat::NUMBERING_SYSTEM_BENGALI => [ + 0 => '০', 1 => '১', 2 => '২', 3 => '৩', 4 => '৪', + 5 => '৫', 6 => '৬', 7 => '৭', 8 => '৮', 9 => '৯', + ], + NumberFormat::NUMBERING_SYSTEM_DEVANAGARI => [ + 0 => '०', 1 => '१', 2 => '२', 3 => '३', 4 => '४', + 5 => '५', 6 => '६', 7 => '७', 8 => '८', 9 => '९', + ], + ]; + + /** + * Formats the number according to the number format. + * + * @param string $number The number. + * @param NumberFormat $numberFormat The number format. + * + * @return string The formatted number. + */ + protected function formatNumber($number, NumberFormat $numberFormat, array $options = []) + { + $parsedPattern = $this->getParsedPattern($numberFormat, $options['style']); + // Start by rounding the number, if rounding is enabled. + if (is_int($options['rounding_mode'])) { + $number = Calculator::round($number, $options['maximum_fraction_digits'], $options['rounding_mode']); + } + $negative = (Calculator::compare('0', $number, 12) == 1); + // Ensure that the value is positive and has the right number of digits. + $signMultiplier = $negative ? '-1' : '1'; + $number = bcdiv($number, $signMultiplier, $options['maximum_fraction_digits']); + // Split the number into major and minor digits. + $numberParts = explode('.', $number); + $majorDigits = $numberParts[0]; + // Account for maximumFractionDigits = 0, where the number won't + // have a decimal point, and $numberParts[1] won't be set. + $minorDigits = isset($numberParts[1]) ? $numberParts[1] : ''; + + if ($options['use_grouping'] && $parsedPattern->isGroupingUsed()) { + // Reverse the major digits, since they are grouped from the right. + $majorDigits = array_reverse(str_split($majorDigits)); + // Group the major digits. + $groups = []; + $groups[] = array_splice($majorDigits, 0, $parsedPattern->getPrimaryGroupSize()); + while (!empty($majorDigits)) { + $groups[] = array_splice($majorDigits, 0, $parsedPattern->getSecondaryGroupSize()); + } + // Reverse the groups and the digits inside of them. + $groups = array_reverse($groups); + foreach ($groups as &$group) { + $group = implode(array_reverse($group)); + } + // Reconstruct the major digits. + $majorDigits = implode(',', $groups); + } + + if ($options['minimum_fraction_digits'] < $options['maximum_fraction_digits']) { + // Strip any trailing zeroes. + $minorDigits = rtrim($minorDigits, '0'); + if (strlen($minorDigits) < $options['minimum_fraction_digits']) { + // Now there are too few digits, re-add trailing zeroes + // until the desired length is reached. + $neededZeroes = $options['minimum_fraction_digits'] - strlen($minorDigits); + $minorDigits .= str_repeat('0', $neededZeroes); + } + } + + // Assemble the final number and insert it into the pattern. + $number = strlen($minorDigits) ? $majorDigits . '.' . $minorDigits : $majorDigits; + $pattern = $negative ? $parsedPattern->getNegativePattern() : $parsedPattern->getPositivePattern(); + $number = preg_replace('/#(?:[\.,]#+)*0(?:[,\.][0#]+)*/', $number, $pattern); + $number = $this->localizeNumber($number, $numberFormat); + + return $number; + } + + /** + * Localizes the number according to the number format. + * + * Both the digits and the symbols are replaced + * with their localized equivalents. + * + * @param string $number The number. + * @param NumberFormat $numberFormat The number format. + * + * @return string The localized number. + * + * @see http://cldr.unicode.org/translation/number-symbols + */ + protected function localizeNumber($number, NumberFormat $numberFormat) + { + // Localize digits. + $numberingSystem = $numberFormat->getNumberingSystem(); + if (isset($this->digits[$numberingSystem])) { + $number = strtr($number, $this->digits[$numberingSystem]); + } + // Localize symbols. + $replacements = [ + '.' => $numberFormat->getDecimalSeparator(), + ',' => $numberFormat->getGroupingSeparator(), + '+' => $numberFormat->getPlusSign(), + '-' => $numberFormat->getMinusSign(), + '%' => $numberFormat->getPercentSign(), + ]; + $number = strtr($number, $replacements); + + return $number; + } + + /** + * Parses the number according to the number format. + * + * Both the digits and the symbols are replaced + * with their non-localized equivalents. + * + * @param string $number The number. + * @param NumberFormat $numberFormat The number format. + * + * @return string The localized number. + */ + protected function parseNumber($number, NumberFormat $numberFormat) + { + $replacements = [ + $numberFormat->getGroupingSeparator() => '', + // Convert the localized symbols back to their original form. + $numberFormat->getDecimalSeparator() => '.', + $numberFormat->getPlusSign() => '+', + $numberFormat->getMinusSign() => '-', + $numberFormat->getPercentSign() => '%', + + // Strip whitespace (spaces and non-breaking spaces). + ' ' => '', + chr(0xC2) . chr(0xA0) => '', + ]; + $numberingSystem = $numberFormat->getNumberingSystem(); + if (isset($this->digits[$numberingSystem])) { + // Convert the localized digits back to latin. + $replacements += array_flip($this->digits[$numberingSystem]); + } + $number = strtr($number, $replacements); + + // Convert the accounting format for negative numbers. + if (substr($number, 0, 1) == '(' && substr($number, -1, 1) == ')') { + $number = '-' . str_replace(['(', ')'], '', $number); + } + // Convert percentages back to their decimal form. + if (strpos($number, '%') !== false) { + $number = str_replace('%', '', $number); + $number = Calculator::divide($number, '100'); + } + + return is_numeric($number) ? $number : false; + } + + /** + * Gets the pattern for the provided number format. + * + * @param NumberFormat $numberFormat The number format. + * @param string $style The formatter style. + * + * @return ParsedPattern + */ + protected function getParsedPattern(NumberFormat $numberFormat, $style) + { + $locale = $numberFormat->getLocale(); + if (!isset($this->parsedPatterns[$locale][$style])) { + $availablePatterns = $this->getAvailablePatterns($numberFormat); + if (!isset($availablePatterns[$style])) { + throw new InvalidArgumentException(sprintf('Unrecognized style "%s".', $style)); + } + + $this->parsedPatterns[$locale][$style] = new ParsedPattern($availablePatterns[$style]); + } + + return $this->parsedPatterns[$locale][$style]; + } + + /** + * Gets the available patterns for the provided number format. + * + * @param NumberFormat $numberFormat The number format. + * + * @return string[] The patterns, keyed by style. + */ + abstract protected function getAvailablePatterns(NumberFormat $numberFormat); +} diff --git a/vendor/commerceguys/intl/src/Formatter/NumberFormatter.php b/vendor/commerceguys/intl/src/Formatter/NumberFormatter.php index 9c8979043..95c9a0020 100644 --- a/vendor/commerceguys/intl/src/Formatter/NumberFormatter.php +++ b/vendor/commerceguys/intl/src/Formatter/NumberFormatter.php @@ -2,420 +2,162 @@ namespace CommerceGuys\Intl\Formatter; -use CommerceGuys\Intl\Currency\CurrencyInterface; +use CommerceGuys\Intl\Calculator; use CommerceGuys\Intl\Exception\InvalidArgumentException; -use CommerceGuys\Intl\NumberFormat\NumberFormatInterface; +use CommerceGuys\Intl\NumberFormat\NumberFormat; +use CommerceGuys\Intl\NumberFormat\NumberFormatRepositoryInterface; /** * Formats numbers using locale-specific patterns. */ class NumberFormatter implements NumberFormatterInterface { - /** - * The number format. - * - * @var NumberFormatInterface - */ - protected $numberFormat; - - /** - * The number pattern used to format positive numbers. - * - * @var string - */ - protected $positivePattern; - - /** - * The number pattern used to format negative numbers. - * - * @var string - */ - protected $negativePattern; + use FormatterTrait; /** - * Whether grouping is used. + * The number format repository. * - * @var bool + * @var NumberFormatRepositoryInterface */ - protected $groupingUsed; + protected $numberFormatRepository; /** - * The size of the group of digits closest to the decimal point. + * The default options. * - * @var int - */ - protected $primaryGroupSize; - - /** - * The size of every group of digits after the primary group. - * - * @var int - */ - protected $secondaryGroupSize; - - /** - * The minimum number of fraction digits to show. - * - * @var int - */ - protected $minimumFractionDigits; - - /** - * The maximum number of fraction digits to show. - * - * @var int - */ - protected $maximumFractionDigits; - - /** - * The currency display style. - * - * @var int + * @var array */ - protected $currencyDisplay; + protected $defaultOptions = [ + 'locale' => 'en', + 'use_grouping' => true, + 'minimum_fraction_digits' => 0, + 'maximum_fraction_digits' => 3, + 'rounding_mode' => PHP_ROUND_HALF_UP, + 'style' => 'decimal', + ]; /** - * Localized digits. + * The loaded number formats. * - * @var array + * @var NumberFormat[] */ - protected $digits = [ - NumberFormatInterface::NUMBERING_SYSTEM_ARABIC => [ - 0 => '٠', 1 => '١', 2 => '٢', 3 => '٣', 4 => '٤', - 5 => '٥', 6 => '٦', 7 => '٧', 8 => '٨', 9 => '٩', - ], - NumberFormatInterface::NUMBERING_SYSTEM_ARABIC_EXTENDED => [ - 0 => '۰', 1 => '۱', 2 => '۲', 3 => '۳', 4 => '۴', - 5 => '۵', 6 => '۶', 7 => '۷', 8 => '۸', 9 => '۹', - ], - NumberFormatInterface::NUMBERING_SYSTEM_BENGALI => [ - 0 => '০', 1 => '১', 2 => '২', 3 => '৩', 4 => '৪', - 5 => '৫', 6 => '৬', 7 => '৭', 8 => '৮', 9 => '৯', - ], - NumberFormatInterface::NUMBERING_SYSTEM_DEVANAGARI => [ - 0 => '०', 1 => '१', 2 => '२', 3 => '३', 4 => '४', - 5 => '५', 6 => '६', 7 => '७', 8 => '८', 9 => '९', - ], - ]; + protected $numberFormats = []; /** * Creates a NumberFormatter instance. * - * @param NumberFormatInterface $numberFormat The number format. - * @param int $style The formatting style. + * @param NumberFormatRepositoryInterface $numberFormatRepository The number format repository. + * @param array $defaultOptions The default options. * * @throws \InvalidArgumentException * @throws \RuntimeException */ - public function __construct(NumberFormatInterface $numberFormat, $style = self::DECIMAL) + public function __construct(NumberFormatRepositoryInterface $numberFormatRepository, array $defaultOptions = []) { if (!extension_loaded('bcmath')) { throw new \RuntimeException('The bcmath extension is required by NumberFormatter.'); } - $availablePatterns = [ - self::DECIMAL => $numberFormat->getDecimalPattern(), - self::PERCENT => $numberFormat->getPercentPattern(), - self::CURRENCY => $numberFormat->getCurrencyPattern(), - self::CURRENCY_ACCOUNTING => $numberFormat->getAccountingCurrencyPattern(), - ]; - if (!array_key_exists($style, $availablePatterns)) { - // Unknown type. - throw new InvalidArgumentException('Unknown format style provided to NumberFormatter::__construct().'); - } - - // Split the selected pattern into positive and negative patterns. - $patterns = explode(';', $availablePatterns[$style]); - if (!isset($patterns[1])) { - // No explicit negative pattern was provided, construct it. - $patterns[1] = '-' . $patterns[0]; - } + $this->validateOptions($defaultOptions); - $this->numberFormat = $numberFormat; - $this->positivePattern = $patterns[0]; - $this->negativePattern = $patterns[1]; - $this->groupingUsed = (strpos($this->positivePattern, ',') !== false); - // This pattern has number groups, parse them. - if ($this->groupingUsed) { - preg_match('/#+0/', $this->positivePattern, $primaryGroupMatches); - $this->primaryGroupSize = $this->secondaryGroupSize = strlen($primaryGroupMatches[0]); - $numberGroups = explode(',', $this->positivePattern); - if (count($numberGroups) > 2) { - // This pattern has a distinct secondary group size. - $this->secondaryGroupSize = strlen($numberGroups[1]); - } - } - - // Initialize the fraction digit settings for decimal and percent - // styles only. The currency ones will default to the currency values. - if (in_array($style, [self::DECIMAL, self::PERCENT])) { - $this->minimumFractionDigits = 0; - $this->maximumFractionDigits = 3; - } - $this->currencyDisplay = self::CURRENCY_DISPLAY_SYMBOL; + $this->numberFormatRepository = $numberFormatRepository; + $this->defaultOptions = array_replace($this->defaultOptions, $defaultOptions); } /** * {@inheritdoc} */ - public function format($value) + public function format($number, array $options = []) { - if (!is_numeric($value)) { - $message = sprintf('The provided value "%s" must be a valid number or numeric string.', $value); + if (!is_numeric($number)) { + $message = sprintf('The provided value "%s" is not a valid number or numeric string.', $number); throw new InvalidArgumentException($message); } + $this->validateOptions($options); + $options = array_replace($this->defaultOptions, $options); - // Ensure that the value is positive and has the right number of digits. - $negative = (bccomp('0', $value, 12) == 1); - $signMultiplier = $negative ? '-1' : '1'; - $value = bcdiv($value, $signMultiplier, $this->maximumFractionDigits); - // Split the number into major and minor digits. - $valueParts = explode('.', $value); - $majorDigits = $valueParts[0]; - // Account for maximumFractionDigits = 0, where the number won't - // have a decimal point, and $valueParts[1] won't be set. - $minorDigits = isset($valueParts[1]) ? $valueParts[1] : ''; - - if ($this->groupingUsed) { - // Reverse the major digits, since they are grouped from the right. - $majorDigits = array_reverse(str_split($majorDigits)); - // Group the major digits. - $groups = []; - $groups[] = array_splice($majorDigits, 0, $this->primaryGroupSize); - while (!empty($majorDigits)) { - $groups[] = array_splice($majorDigits, 0, $this->secondaryGroupSize); - } - // Reverse the groups and the digits inside of them. - $groups = array_reverse($groups); - foreach ($groups as &$group) { - $group = implode(array_reverse($group)); - } - // Reconstruct the major digits. - $majorDigits = implode(',', $groups); - } - - if ($this->minimumFractionDigits < $this->maximumFractionDigits) { - // Strip any trailing zeroes. - $minorDigits = rtrim($minorDigits, '0'); - if (strlen($minorDigits) < $this->minimumFractionDigits) { - // Now there are too few digits, re-add trailing zeroes - // until the desired length is reached. - $neededZeroes = $this->minimumFractionDigits - strlen($minorDigits); - $minorDigits .= str_repeat('0', $neededZeroes); - } + $number = (string) $number; + // Percentages are passed as decimals (e.g. 0.2 for 20%). + if ($options['style'] == 'percent') { + $number = Calculator::multiply($number, '100'); } + $numberFormat = $this->getNumberFormat($options['locale']); + $number = $this->formatNumber($number, $numberFormat, $options); - // Assemble the final number and insert it into the pattern. - $value = strlen($minorDigits) ? $majorDigits . '.' . $minorDigits : $majorDigits; - $pattern = $negative ? $this->negativePattern : $this->positivePattern; - $value = preg_replace('/#(?:[\.,]#+)*0(?:[,\.][0#]+)*/', $value, $pattern); - - // Localize the number. - $value = $this->replaceDigits($value); - $value = $this->replaceSymbols($value); - - return $value; + return $number; } /** * {@inheritdoc} */ - public function formatCurrency($value, CurrencyInterface $currency) + public function parse($number, array $options = []) { - // Use the currency defaults if the values weren't set by the caller. - $resetMinimumFractionDigits = $resetMaximumFractionDigits = false; - if (!isset($this->minimumFractionDigits)) { - $this->minimumFractionDigits = $currency->getFractionDigits(); - $resetMinimumFractionDigits = true; - } - if (!isset($this->maximumFractionDigits)) { - $this->maximumFractionDigits = $currency->getFractionDigits(); - $resetMaximumFractionDigits = true; - } - - // Format the decimal part of the value first. - $value = $this->format($value); - - // Reset the fraction digit settings, so that they don't affect - // future formattings with different currencies. - if ($resetMinimumFractionDigits) { - $this->minimumFractionDigits = null; - } - if ($resetMaximumFractionDigits) { - $this->maximumFractionDigits = null; - } + $this->validateOptions($options); + $options = array_replace($this->defaultOptions, $options); + $numberFormat = $this->getNumberFormat($options['locale']); + $number = $this->parseNumber($number, $numberFormat); - // Determine whether to show the currency symbol or the currency code. - if ($this->currencyDisplay == self::CURRENCY_DISPLAY_SYMBOL) { - $symbol = $currency->getSymbol(); - } else { - $symbol = $currency->getCurrencyCode(); - } - - return str_replace('¤', $symbol, $value); + return $number; } /** - * {@inheritdoc} + * Gets the number format for the provided locale. + * + * @param string $locale The locale. + * + * @return NumberFormat */ - public function parse($value) + protected function getNumberFormat($locale) { - $replacements = [ - $this->numberFormat->getGroupingSeparator() => '', - // Convert the localized symbols back to their original form. - $this->numberFormat->getDecimalSeparator() => '.', - $this->numberFormat->getPlusSign() => '+', - $this->numberFormat->getMinusSign() => '-', - - // Strip whitespace (spaces and non-breaking spaces). - ' ' => '', - chr(0xC2) . chr(0xA0) => '', - ]; - $numberingSystem = $this->numberFormat->getNumberingSystem(); - if (isset($this->digits[$numberingSystem])) { - // Convert the localized digits back to latin. - $replacements += array_flip($this->digits[$numberingSystem]); - } - - $value = strtr($value, $replacements); - if (substr($value, 0, 1) == '(' && substr($value, -1, 1) == ')') { - // This is an accounting formatted negative number. - $value = '-' . str_replace(['(', ')'], '', $value); + if (!isset($this->numberFormats[$locale])) { + $this->numberFormats[$locale] = $this->numberFormatRepository->get($locale); } - return is_numeric($value) ? $value : false; + return $this->numberFormats[$locale]; } /** * {@inheritdoc} */ - public function parseCurrency($value, CurrencyInterface $currency) + protected function getAvailablePatterns(NumberFormat $numberFormat) { - $replacements = [ - // Strip the currency code or symbol. - $currency->getCurrencyCode() => '', - $currency->getSymbol() => '', + return [ + 'decimal' => $numberFormat->getDecimalPattern(), + 'percent' => $numberFormat->getPercentPattern(), ]; - $value = strtr($value, $replacements); - - return $this->parse($value); } /** - * Replaces digits with their localized equivalents. + * Validates the provided options. * - * @param string $value The value being formatted. + * Ensures the absence of unknown keys, correct data types and values. * - * @return string - */ - protected function replaceDigits($value) - { - $numberingSystem = $this->numberFormat->getNumberingSystem(); - if (isset($this->digits[$numberingSystem])) { - $value = strtr($value, $this->digits[$numberingSystem]); - } - - return $value; - } - - /** - * Replaces number symbols with their localized equivalents. + * @param array $options The options. * - * @param string $value The value being formatted. - * - * @return string - * - * @see http://cldr.unicode.org/translation/number-symbols + * @throws \InvalidArgumentException */ - protected function replaceSymbols($value) + protected function validateOptions(array $options) { - $replacements = [ - '.' => $this->numberFormat->getDecimalSeparator(), - ',' => $this->numberFormat->getGroupingSeparator(), - '+' => $this->numberFormat->getPlusSign(), - '-' => $this->numberFormat->getMinusSign(), - '%' => $this->numberFormat->getPercentSign(), + foreach ($options as $option => $value) { + if (!array_key_exists($option, $this->defaultOptions)) { + throw new InvalidArgumentException(sprintf('Unrecognized option "%s".', $option)); + } + } + if (isset($options['use_grouping']) && !is_bool($options['use_grouping'])) { + throw new InvalidArgumentException('The option "use_grouping" must be a boolean.'); + } + foreach (['minimum_fraction_digits', 'maximum_fraction_digits'] as $option) { + if (array_key_exists($option, $options) && !is_numeric($options[$option])) { + throw new InvalidArgumentException(sprintf('The option "%s" must be numeric.', $option)); + } + } + $roundingModes = [ + PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, + PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD, 'none', ]; - - return strtr($value, $replacements); - } - - /** - * {@inheritdoc} - */ - public function getNumberFormat() - { - return $this->numberFormat; - } - - /** - * {@inheritdoc} - */ - public function getMinimumFractionDigits() - { - return $this->minimumFractionDigits; - } - - /** - * {@inheritdoc} - */ - public function setMinimumFractionDigits($minimumFractionDigits) - { - $this->minimumFractionDigits = $minimumFractionDigits; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getMaximumFractionDigits() - { - return $this->maximumFractionDigits; - } - - /** - * {@inheritdoc} - */ - public function setMaximumFractionDigits($maximumFractionDigits) - { - $this->maximumFractionDigits = $maximumFractionDigits; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function isGroupingUsed() - { - return $this->groupingUsed; - } - - /** - * {@inheritdoc} - */ - public function setGroupingUsed($groupingUsed) - { - $this->groupingUsed = $groupingUsed; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getCurrencyDisplay() - { - return $this->currencyDisplay; - } - - /** - * {@inheritdoc} - */ - public function setCurrencyDisplay($currencyDisplay) - { - $this->currencyDisplay = $currencyDisplay; - - return $this; + if (!empty($options['rounding_mode']) && !in_array($options['rounding_mode'], $roundingModes)) { + throw new InvalidArgumentException(sprintf('Unrecognized rounding mode "%s".', $options['rounding_mode'])); + } + if (!empty($options['style']) && !in_array($options['style'], ['decimal', 'percent'])) { + throw new InvalidArgumentException(sprintf('Unrecognized style "%s".', $options['style'])); + } } } diff --git a/vendor/commerceguys/intl/src/Formatter/NumberFormatterInterface.php b/vendor/commerceguys/intl/src/Formatter/NumberFormatterInterface.php index 3a510be2a..ce6207f52 100644 --- a/vendor/commerceguys/intl/src/Formatter/NumberFormatterInterface.php +++ b/vendor/commerceguys/intl/src/Formatter/NumberFormatterInterface.php @@ -2,150 +2,48 @@ namespace CommerceGuys\Intl\Formatter; -use CommerceGuys\Intl\Currency\CurrencyInterface; -use CommerceGuys\Intl\NumberFormat\NumberFormatInterface; +use CommerceGuys\Intl\Currency\Currency; +use CommerceGuys\Intl\NumberFormat\NumberFormat; interface NumberFormatterInterface { - /* Format style constants */ - const DECIMAL = 1; - const PERCENT = 2; - const CURRENCY = 3; - const CURRENCY_ACCOUNTING = 4; - - /* Currency display style constants */ - const CURRENCY_DISPLAY_SYMBOL = 1; - const CURRENCY_DISPLAY_CODE = 2; - /** * Formats a number. * - * Please note that the provided value should already be rounded. - * This formatter doesn't do any rounding of its own, and will simply - * truncate extra digits. - * - * @param string $value The value to format. - * - * @return string - */ - public function format($value); - - /** - * Formats a currency value. - * - * Please note that the provided value should already be rounded. - * This formatter doesn't do any rounding of its own, and will simply - * truncate extra digits. + * Supported options: + * - locale: The locale. Default: 'en'. + * - use_grouping: Whether to use grouping separators, + * such as thousands separators. + * Default: true. + * - minimum_fraction_digits: Minimum fraction digits. Default: 0. + * - maximum_fraction_digits: Minimum fraction digits. Default: 3. + * - rounding_mode: The rounding mode. + * A PHP_ROUND_ constant or 'none' to skip + * rounding. Default: PHP_ROUND_HALF_UP. + * - style: The style. + * One of: 'decimal', 'percent'. + * Default: 'decimal'. * - * @param string $value The value to format. - * @param CurrencyInterface $currency The currency. + * @param string $number The number. + * @param array $options The formatting options. * - * @return string + * @return string The formatted number. */ - public function formatCurrency($value, CurrencyInterface $currency); + public function format($number, array $options = []); /** * Parses a number. * * Commonly used in input widgets where the end-user might input - * a value using digits and symbols common to their locale. - * - * @param string $value The value to parse. - * - * @return string|false The parsed numeric value or FALSE on error. - */ - public function parse($value); - - /** - * Parses a formatted currency value. - * - * @param string $value The value to parse. - * @param CurrencyInterface $currency The currency. - * - * @return string|false The parsed numeric value or FALSE on error. - */ - public function parseCurrency($value, CurrencyInterface $currency); - - /** - * Gets the number format. - * - * @return NumberFormatInterface - */ - public function getNumberFormat(); - - /** - * Gets the minimum number of fraction digits. - * - * Defaults to 0 for decimal and percentage styles. - * Defaults to null for currency styles, since the currency number of - * fraction digits is used as the default in that case. - * - * @return int - */ - public function getMinimumFractionDigits(); - - /** - * Sets the minimum number of fraction digits. - * - * @param int $minimumFractionDigits + * a number using digits and symbols common to their locale. * - * @return self - */ - public function setMinimumFractionDigits($minimumFractionDigits); - - /** - * Gets the maximum number of fraction digits. - * - * Defaults to 3 for decimal and percentage styles. - * Defaults to null for currency styles, since the currency number of - * fraction digits is used as the default in that case. - * - * @return int - */ - public function getMaximumFractionDigits(); - - /** - * Sets the maximum number of fraction digits. - * - * @param int $maximumFractionDigits - * - * @return self - */ - public function setMaximumFractionDigits($maximumFractionDigits); - - /** - * Returns whether the major digits will be grouped. - * - * @return bool - */ - public function isGroupingUsed(); - - /** - * Sets whether or not major digits should be grouped. - * - * @param bool $groupingUsed - * - * @return self - */ - public function setGroupingUsed($groupingUsed); - - /** - * Gets the currency display style. - * - * Controls whether a currency amount will be shown with the - * currency symbol (CURRENCY_DISPLAY_SYMBOL) or the - * currency code (CURRENCY_DISPLAY_CODE). - * - * @return int - */ - public function getCurrencyDisplay(); - - /** - * Sets the currency display style. + * Supported options: + * - locale: The locale. Default: 'en'. * - * @param int $currencyDisplay One of the CURRENCY_DISPLAY_ constants. + * @param string $number The formatted number. + * @param array $options The parsing options. * - * @return self + * @return string|false The parsed number or FALSE on error. */ - public function setCurrencyDisplay($currencyDisplay); + public function parse($number, array $options = []); } diff --git a/vendor/commerceguys/intl/src/Formatter/ParsedPattern.php b/vendor/commerceguys/intl/src/Formatter/ParsedPattern.php new file mode 100644 index 000000000..aa6e5f43e --- /dev/null +++ b/vendor/commerceguys/intl/src/Formatter/ParsedPattern.php @@ -0,0 +1,129 @@ +<?php + +namespace CommerceGuys\Intl\Formatter; + +/** + * Represents a parsed number pattern. + */ +final class ParsedPattern +{ + /** + * The positive number pattern. + * + * @var string + */ + protected $positivePattern; + + /** + * The negative number pattern. + * + * @var string + */ + protected $negativePattern; + + /** + * Whether grouping is used. + * + * @var bool + */ + protected $groupingUsed; + + /** + * The primary group size. + * + * @var int + */ + protected $primaryGroupSize; + + /** + * The secondary group size. + * + * @var int + */ + protected $secondaryGroupSize; + + /** + * Creates a new ParsedPattern instance. + * + * @param string $pattern The raw pattern. + */ + public function __construct($pattern) + { + // Split the pattern into positive and negative patterns. + $patternList = explode(';', $pattern); + if (!isset($patternList[1])) { + // No explicit negative pattern was provided, construct it. + $patternList[1] = '-' . $patternList[0]; + } + + $this->positivePattern = $patternList[0]; + $this->negativePattern = $patternList[1]; + $this->groupingUsed = (strpos($patternList[0], ',') !== false); + if ($this->groupingUsed) { + preg_match('/#+0/', $patternList[0], $primaryGroupMatches); + $this->primaryGroupSize = $this->secondaryGroupSize = strlen($primaryGroupMatches[0]); + $numberGroups = explode(',', $patternList[0]); + if (count($numberGroups) > 2) { + // This pattern has a distinct secondary group size. + $this->secondaryGroupSize = strlen($numberGroups[1]); + } + } + } + + /** + * Gets the positive number pattern. + * + * Used to format positive numbers. + * + * @return string + */ + public function getPositivePattern() + { + return $this->positivePattern; + } + + /** + * Gets the negative number pattern. + * + * Used to format negative numbers. + * + * @return string + */ + public function getNegativePattern() + { + return $this->negativePattern; + } + + /** + * Gets whether grouping is used. + * + * Indicates that major digits should be grouped according to + * group sizes, right-to-left. + * + * @return bool + */ + public function isGroupingUsed() + { + return $this->groupingUsed; + } + + /** + * Gets the primary group size. + * + * @return int|null + */ + public function getPrimaryGroupSize() + { + return $this->primaryGroupSize; + } + + /** + * Gets the secondary group size. + * + * @return int|null + */ + public function getSecondaryGroupSize() + { + return $this->secondaryGroupSize; + } +} |