Newer
Older
<?php
/**
* Piwik - free/libre analytics platform
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik;
use Piwik\Container\StaticContainer;
use Piwik\Translation\Translator;
/**
* Class NumberFormatter
*
* Used to format numbers according to current language
*/
class NumberFormatter
/** @var string language specific patterns for numbers */
protected $patternNumber;
/** @var string language specific pattern for percent numbers */
protected $patternPercent;
/** @var string language specific pattern for currency numbers */
protected $patternCurrency;
/** @var string language specific plus sign */
protected $symbolPlus;
/** @var string language specific minus sign */
protected $symbolMinus;
/** @var string language specific percent sign */
protected $symbolPercent;
/** @var string language specific symbol used as decimal separator */
protected $symbolDecimal;
/** @var string language specific symbol used as group separator */
protected $symbolGroup;
/** @var bool indicates if language uses grouping for numbers */
protected $usesGrouping;
/** @var int language specific size for primary group numbers */
protected $primaryGroupSize;
/** @var int language specific size for secondary group numbers */
protected $secondaryGroupSize;
/**
* Loads all required data from Intl plugin
* TODO: instead of going directly through Translator, there should be a specific class
* that gets needed characters (ie, NumberFormatSource). The default implementation
* can use the Translator. This will make it easier to unit test NumberFormatter,
* w/o needing the Piwik Environment.
*
* @return NumberFormatter
public function __construct(Translator $translator)
$this->patternNumber = $translator->translate('Intl_NumberFormatNumber');
$this->patternCurrency = $translator->translate('Intl_NumberFormatCurrency');
$this->patternPercent = $translator->translate('Intl_NumberFormatPercent');
$this->symbolPlus = $translator->translate('Intl_NumberSymbolPlus');
$this->symbolMinus = $translator->translate('Intl_NumberSymbolMinus');
$this->symbolPercent = $translator->translate('Intl_NumberSymbolPercent');
$this->symbolGroup = $translator->translate('Intl_NumberSymbolGroup');
$this->symbolDecimal = $translator->translate('Intl_NumberSymbolDecimal');
/**
* Parses the given pattern and returns patterns for positive and negative numbers
*
* @param string $pattern
* @return array
*/
protected function parsePattern($pattern)
{
$patterns = explode(';', $pattern);
if (!isset($patterns[1])) {
// No explicit negative pattern was provided, construct it.
$patterns[1] = '-' . $patterns[0];
* Formats a given number or percent value (if $value starts or ends with a %)
*
* @param string|int|float $value
* @param int $maximumFractionDigits
* @param int $minimumFractionDigits
* @return mixed|string
*/
public function format($value, $maximumFractionDigits=0, $minimumFractionDigits=0)
{
if (is_string($value)
&& trim($value, '%') != $value
) {
return $this->formatPercent($value, $maximumFractionDigits, $minimumFractionDigits);
return $this->formatNumber($value, $maximumFractionDigits, $minimumFractionDigits);
}
/**
* Formats a given number
*
* @see \Piwik\NumberFormatter::format()
*
* @param string|int|float $value
* @param int $maximumFractionDigits
* @param int $minimumFractionDigits
* @return mixed|string
*/
public function formatNumber($value, $maximumFractionDigits=0, $minimumFractionDigits=0)
{
static $positivePattern, $negativePattern;
if (empty($positivePatter) || empty($negativePattern)) {
list($positivePattern, $negativePattern) = $this->parsePattern($this->patternNumber);
}
$negative = $this->isNegative($value);
$pattern = $negative ? $negativePattern : $positivePattern;
return $this->formatNumberWithPattern($pattern, $value, $maximumFractionDigits, $minimumFractionDigits);
}
/**
* Formats given number as percent value
* @param string|int|float $value
* @param int $maximumFractionDigits
* @param int $minimumFractionDigits
* @return mixed|string
*/
public function formatPercent($value, $maximumFractionDigits=0, $minimumFractionDigits=0)
{
static $positivePattern, $negativePattern;
if (empty($positivePatter) || empty($negativePattern)) {
list($positivePattern, $negativePattern) = $this->parsePattern($this->patternPercent);
}
$newValue = trim($value, " \0\x0B%");
if (!is_numeric($newValue)) {
return $value;
}
$negative = $this->isNegative($value);
$pattern = $negative ? $negativePattern : $positivePattern;
return $this->formatNumberWithPattern($pattern, $newValue, $maximumFractionDigits, $minimumFractionDigits);
/**
* Formats given number as percent value, but keep the leading + sign if found
*
* @param $value
* @return string
*/
public function formatPercentEvolution($value)
{
$isPositiveEvolution = !empty($value) && ($value > 0 || $value[0] == '+');
$formatted = self::formatPercent($value);
if($isPositiveEvolution) {
return $this->symbolPlus . $formatted;
}
return $formatted;
}
/**
* Formats given number as percent value
* @param string|int|float $value
* @param string $currency
* @param int $precision
* @return mixed|string
*/
public function formatCurrency($value, $currency, $precision=2)
{
static $positivePattern, $negativePattern;
if (empty($positivePatter) || empty($negativePattern)) {
list($positivePattern, $negativePattern) = $this->parsePattern($this->patternCurrency);
}
$newValue = trim($value, " \0\x0B$currency");
if (!is_numeric($newValue)) {
return $value;
}
$negative = $this->isNegative($value);
$pattern = $negative ? $negativePattern : $positivePattern;
if ($newValue == round($newValue)) {
// if no fraction digits available, don't show any
$value = $this->formatNumberWithPattern($pattern, $newValue, 0, 0);
} else {
// show given count of fraction digits otherwise
$value = $this->formatNumberWithPattern($pattern, $newValue, $precision, $precision);
}
return str_replace('¤', $currency, $value);
}
/**
* Formats the given number with the given pattern
*
* @param string $pattern
* @param string|int|float $value
* @param int $maximumFractionDigits
* @param int $minimumFractionDigits
* @return mixed|string
*/
protected function formatNumberWithPattern($pattern, $value, $maximumFractionDigits=0, $minimumFractionDigits=0)
{
if (!is_numeric($value)) {
return $value;
}
$this->usesGrouping = (strpos($pattern, ',') !== false);
// if pattern has number groups, parse them.
if ($this->usesGrouping) {
preg_match('/#+0/', $pattern, $primaryGroupMatches);
$this->primaryGroupSize = $this->secondaryGroupSize = strlen($primaryGroupMatches[0]);
$numberGroups = explode(',', $pattern);
// check for distinct secondary group size.
if (count($numberGroups) > 2) {
$this->secondaryGroupSize = strlen($numberGroups[1]);
}
}
// Ensure that the value is positive and has the right number of digits.
$negative = $this->isNegative($value);
$signMultiplier = $negative ? '-1' : '1';
$value = $value / $signMultiplier;
$value = round($value, $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->usesGrouping) {
// Reverse the major digits, since they are grouped from the right.
$majorDigits = array_reverse(str_split($majorDigits));
// Group the major digits.
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
$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 ($minimumFractionDigits < $maximumFractionDigits) {
// Strip any trailing zeroes.
$minorDigits = rtrim($minorDigits, '0');
if (strlen($minorDigits) < $minimumFractionDigits) {
// Now there are too few digits, re-add trailing zeroes
// until the desired length is reached.
$neededZeroes = $minimumFractionDigits - strlen($minorDigits);
$minorDigits .= str_repeat('0', $neededZeroes);
}
}
// Assemble the final number and insert it into the pattern.
$value = $minorDigits ? $majorDigits . '.' . $minorDigits : $majorDigits;
$value = preg_replace('/#(?:[\.,]#+)*0(?:[,\.][0#]+)*/', $value, $pattern);
// Localize the number.
$value = $this->replaceSymbols($value);
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)
{
'.' => $this->symbolDecimal,
',' => $this->symbolGroup,
'+' => $this->symbolPlus,
'-' => $this->symbolMinus,
'%' => $this->symbolPercent,
return strtr($value, $replacements);
}
/**
* @param $value
* @return bool
*/
protected function isNegative($value)
{
return $value < 0;
}
/**
* @deprecated
* @return self
*/
public static function getInstance()
{
return StaticContainer::get('Piwik\NumberFormatter');
}