aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/commerceguys/intl/src/Formatter
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/commerceguys/intl/src/Formatter')
-rw-r--r--vendor/commerceguys/intl/src/Formatter/CurrencyFormatter.php241
-rw-r--r--vendor/commerceguys/intl/src/Formatter/CurrencyFormatterInterface.php54
-rw-r--r--vendor/commerceguys/intl/src/Formatter/FormatterTrait.php215
-rw-r--r--vendor/commerceguys/intl/src/Formatter/NumberFormatter.php426
-rw-r--r--vendor/commerceguys/intl/src/Formatter/NumberFormatterInterface.php154
-rw-r--r--vendor/commerceguys/intl/src/Formatter/ParsedPattern.php129
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;
+ }
+}