aboutsummaryrefslogtreecommitdiffstats
path: root/library/intl/src/Formatter/NumberFormatter.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/intl/src/Formatter/NumberFormatter.php')
-rw-r--r--library/intl/src/Formatter/NumberFormatter.php406
1 files changed, 406 insertions, 0 deletions
diff --git a/library/intl/src/Formatter/NumberFormatter.php b/library/intl/src/Formatter/NumberFormatter.php
new file mode 100644
index 000000000..0b8e2ae59
--- /dev/null
+++ b/library/intl/src/Formatter/NumberFormatter.php
@@ -0,0 +1,406 @@
+<?php
+
+namespace CommerceGuys\Intl\Formatter;
+
+use CommerceGuys\Intl\Currency\CurrencyInterface;
+use CommerceGuys\Intl\Exception\InvalidArgumentException;
+use CommerceGuys\Intl\NumberFormat\NumberFormatInterface;
+
+/**
+ * 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;
+
+ /**
+ * Whether grouping is used.
+ *
+ * @var bool
+ */
+ protected $groupingUsed;
+
+ /**
+ * The size of the group of digits closest to the decimal point.
+ *
+ * @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
+ */
+ protected $currencyDisplay;
+
+ /**
+ * Localized digits.
+ *
+ * @var array
+ */
+ protected $digits = array(
+ NumberFormatInterface::NUMBERING_SYSTEM_ARABIC => array(
+ 0 => '٠', 1 => '١', 2 => '٢', 3 => '٣', 4 => '٤',
+ 5 => '٥', 6 => '٦', 7 => '٧', 8 => '٨', 9 => '٩',
+ ),
+ NumberFormatInterface::NUMBERING_SYSTEM_ARABIC_EXTENDED => array(
+ 0 => '۰', 1 => '۱', 2 => '۲', 3 => '۳', 4 => '۴',
+ 5 => '۵', 6 => '۶', 7 => '۷', 8 => '۸', 9 => '۹',
+ ),
+ NumberFormatInterface::NUMBERING_SYSTEM_BENGALI => array(
+ 0 => '০', 1 => '১', 2 => '২', 3 => '৩', 4 => '৪',
+ 5 => '৫', 6 => '৬', 7 => '৭', 8 => '৮', 9 => '৯',
+ ),
+ NumberFormatInterface::NUMBERING_SYSTEM_DEVANAGARI => array(
+ 0 => '०', 1 => '१', 2 => '२', 3 => '३', 4 => '४',
+ 5 => '५', 6 => '६', 7 => '७', 8 => '८', 9 => '९',
+ ),
+ );
+
+ /**
+ * Creaes a NumberFormatter instance.
+ *
+ * @param NumberFormatInterface $numberFormat The number format.
+ * @param int $style The formatting style.
+ *
+ * @throws InvalidArgumentException
+ */
+ public function __construct(NumberFormatInterface $numberFormat, $style = self::DECIMAL)
+ {
+ $availablePatterns = array(
+ 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->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, array(self::DECIMAL, self::PERCENT))) {
+ $this->minimumFractionDigits = 0;
+ $this->maximumFractionDigits = 3;
+ }
+ $this->currencyDisplay = self::CURRENCY_DISPLAY_SYMBOL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function format($value)
+ {
+ if (!is_numeric($value)) {
+ $message = sprintf('The provided value "%s" must be a valid number or numeric string.', $value);
+ throw new InvalidArgumentException($message);
+ }
+
+ // 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 = array();
+ $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);
+ }
+ }
+
+ // Assemble the final number and insert it into the pattern.
+ $value = $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;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formatCurrency($value, CurrencyInterface $currency)
+ {
+ // 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;
+ }
+
+ // 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);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function parseCurrency($value, CurrencyInterface $currency)
+ {
+ $replacements = array(
+ // Convert the localized symbols back to their original form.
+ $this->numberFormat->getDecimalSeparator() => '.',
+ $this->numberFormat->getPlusSign() => '+',
+ $this->numberFormat->getMinusSign() => '-',
+
+ // Strip any grouping separators, the currency code or symbol.
+ $this->numberFormat->getGroupingSeparator() => '',
+ $currency->getCurrencyCode() => '',
+ $currency->getSymbol() => '',
+
+ // 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(array('(', ')'), '', $value);
+ }
+
+ return is_numeric($value) ? $value : false;
+ }
+
+ /**
+ * Replaces digits with their localized equivalents.
+ *
+ * @param string $value The value being formatted.
+ *
+ * @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 string $value The value being formatted.
+ *
+ * @return string
+ *
+ * @see http://cldr.unicode.org/translation/number-symbols
+ */
+ protected function replaceSymbols($value)
+ {
+ $replacements = array(
+ '.' => $this->numberFormat->getDecimalSeparator(),
+ ',' => $this->numberFormat->getGroupingSeparator(),
+ '+' => $this->numberFormat->getPlusSign(),
+ '-' => $this->numberFormat->getMinusSign(),
+ '%' => $this->numberFormat->getPercentSign(),
+ );
+
+ 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;
+ }
+}