diff options
Diffstat (limited to 'www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php')
-rw-r--r-- | www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php new file mode 100644 index 00000000..a3bb90e3 --- /dev/null +++ b/www/wiki/extensions/SemanticMediaWiki/src/DataValues/Number/IntlNumberFormatter.php @@ -0,0 +1,384 @@ +<?php + +namespace SMW\DataValues\Number; + +use InvalidArgumentException; +use SMW\Message; +use SMW\Options; +use SMWNumberValue as NumberValue; + +/** + * @license GNU GPL v2+ + * @since 2.1 + * + * @author mwjames + * @author Markus Krötzsch + */ +class IntlNumberFormatter { + + /** + * Localization related constants + */ + const CONTENT_LANGUAGE = Message::CONTENT_LANGUAGE; + const USER_LANGUAGE = Message::USER_LANGUAGE; + const PREFERRED_LANGUAGE = 'preferred.language'; + + /** + * Separator related constants + */ + const DECIMAL_SEPARATOR = NumberValue::DECIMAL_SEPARATOR; + const THOUSANDS_SEPARATOR = NumberValue::THOUSANDS_SEPARATOR; + + /** + * Format related constants + */ + const DEFAULT_FORMAT = 'default.format'; + const VALUE_FORMAT = 'value.format'; + + /** + * @var IntlNumberFormatter + */ + private static $instance = null; + + /** + * @var Options + */ + private $options = null; + + /** + * @var integer + */ + private $maxNonExpNumber = null; + + /** + * @var integer + */ + private $defaultPrecision = 3; + + /** + * @since 2.1 + * + * @param integer $maxNonExpNumber + */ + public function __construct( $maxNonExpNumber ) { + $this->maxNonExpNumber = $maxNonExpNumber; + $this->options = new Options(); + } + + /** + * @since 2.1 + * + * @return IntlNumberFormatter + */ + public static function getInstance() { + + if ( self::$instance === null ) { + self::$instance = new self( + $GLOBALS['smwgMaxNonExpNumber'] + ); + } + + return self::$instance; + } + + /** + * @since 2.1 + */ + public function clear() { + self::$instance = null; + } + + /** + * @since 2.4 + */ + public function reset() { + $this->options->set( self::DECIMAL_SEPARATOR, false ); + $this->options->set( self::THOUSANDS_SEPARATOR, false ); + $this->options->set( self::USER_LANGUAGE, false ); + $this->options->set( self::CONTENT_LANGUAGE, false ); + $this->options->set( self::PREFERRED_LANGUAGE, false ); + } + + /** + * @since 2.4 + * + * @return string $key + * @param mixed $value + */ + public function setOption( $key, $value ) { + $this->options->set( $key, $value ); + } + + /** + * @since 2.4 + * + * @param integer $type + * @param string|integer $locale + * + * @return string + */ + public function getSeparatorByLanguage( $type, $locale = '' ) { + + $language = $locale === self::USER_LANGUAGE ? $this->getUserLanguage() : $this->getContentLanguage(); + + if ( $type === self::DECIMAL_SEPARATOR ) { + return $this->getPreferredLocalizedSeparator( self::DECIMAL_SEPARATOR, 'smw_decseparator', $language ); + } + + if ( $type === self::THOUSANDS_SEPARATOR ) { + return $this->getPreferredLocalizedSeparator( self::THOUSANDS_SEPARATOR, 'smw_kiloseparator', $language ); + } + + throw new InvalidArgumentException( $type . " is unknown" ); + } + + /** + * This method formats a float number value according to the given language and + * precision settings, with some intelligence to produce readable output. Used + * to format a number that was not hand-formatted by a user. + * + * @param mixed $value input number + * @param integer|false $precision optional positive integer, controls how many digits after + * the decimal point are shown + * @param string|integer $format + * + * @since 2.1 + * + * @return string + */ + public function format( $value, $precision = false, $format = '' ) { + + if ( $format === self::VALUE_FORMAT ) { + return $this->getValueFormattedNumberWithPrecision( $value, $precision ); + } + + if ( $precision !== false || $format === self::DEFAULT_FORMAT ) { + return $this->getDefaultFormattedNumberWithPrecision( $value, $precision ); + } + + return $this->doFormatByHeuristicRuleWith( $value, $precision ); + } + + /** + * This method formats a float number value according to the given language and + * precision settings, with some intelligence to produce readable output. Used + * to format a number that was not hand-formatted by a user. + * + * @param mixed $value input number + * @param integer|false $precision optional positive integer, controls how many digits after + * the decimal point are shown + * + * @since 2.1 + * + * @return string + */ + private function doFormatByHeuristicRuleWith( $value, $precision = false ) { + + // BC configuration to keep default behaviour + $precision = $this->defaultPrecision; + + $decseparator = $this->getSeparatorByLanguage( + self::DECIMAL_SEPARATOR, + self::USER_LANGUAGE + ); + + // If number is a trillion or more, then switch to scientific + // notation. If number is less than 0.0000001 (i.e. twice precision), + // then switch to scientific notation. Otherwise print number + // using number_format. This may lead to 1.200, so then use trim to + // remove trailing zeroes. + $doScientific = false; + + // @todo: Don't do all this magic for integers, since the formatting does not fit there + // correctly. E.g. one would have integers formatted as 1234e6, not as 1.234e9, right? + // The "$value!=0" is relevant: we want to scientify numbers that are close to 0, but never 0! + if ( ( $precision > 0 ) && ( $value != 0 ) ) { + $absValue = abs( $value ); + if ( $absValue >= $this->maxNonExpNumber ) { + $doScientific = true; + } elseif ( $absValue < pow( 10, - $precision ) ) { + $doScientific = true; + } elseif ( $absValue < 1 ) { + if ( $absValue < pow( 10, - $precision ) ) { + $doScientific = true; + } else { + // Increase decimal places for small numbers, e.g. .00123 should be 5 places. + for ( $i = 0.1; $absValue <= $i; $i *= 0.1 ) { + $precision++; + } + } + } + } + + if ( $doScientific ) { + // Should we use decimal places here? + $value = sprintf( "%1.6e", $value ); + // Make it more readable by removing trailing zeroes from n.n00e7. + $value = preg_replace( '/(\\.\\d+?)0*e/u', '${1}e', $value, 1 ); + // NOTE: do not use the optional $count parameter with preg_replace. We need to + // remain compatible with PHP 4.something. + if ( $decseparator !== '.' ) { + $value = str_replace( '.', $decseparator, $value ); + } + } else { + $value = $this->doFormatWithPrecision( + $value, + $precision, + $decseparator, + $this->getSeparatorByLanguage( self::THOUSANDS_SEPARATOR, self::USER_LANGUAGE ) + ); + + // Make it more readable by removing ending .000 from nnn.000 + // Assumes substr is faster than a regular expression replacement. + $end = $decseparator . str_repeat( '0', $precision ); + $lenEnd = strlen( $end ); + + if ( substr( $value, - $lenEnd ) === $end ) { + $value = substr( $value, 0, - $lenEnd ); + } else { + $decseparator = preg_quote( $decseparator, '/' ); + // If above replacement occurred, no need to do the next one. + // Make it more readable by removing trailing zeroes from nn.n00. + $value = preg_replace( "/($decseparator\\d+?)0*$/u", '$1', $value, 1 ); + } + } + + return $value; + } + + private function getValueFormattedNumberWithPrecision( $value, $precision = false ) { + + // The decimal are in ISO format (.), the separator as plain representation + // may collide with the content language (FR) therefore use the content language + // to match the decimal separator + if ( $this->isScientific( $value ) ) { + return $this->doFormatExponentialNotation( $value ); + } + + if ( $precision === false || $precision === null ) { + $precision = $this->getPrecisionFrom( $value ); + } + + return $this->doFormatWithPrecision( + $value, + $precision, + $this->getSeparatorByLanguage( self::DECIMAL_SEPARATOR, self::CONTENT_LANGUAGE ), + '' + ); + } + + private function getDefaultFormattedNumberWithPrecision( $value, $precision = false ) { + + if ( $precision === false ) { + return $this->isDecimal( $value ) ? $this->applyDefaultPrecision( $value ) : floatval( $value ); + } + + return $this->doFormatWithPrecision( + $value, + $precision, + $this->getSeparatorByLanguage( self::DECIMAL_SEPARATOR, self::USER_LANGUAGE ), + $this->getSeparatorByLanguage( self::THOUSANDS_SEPARATOR, self::USER_LANGUAGE ) + ); + } + + private function isDecimal( $value ) { + return floor( $value ) !== $value; + } + + private function isScientific( $value ) { + return strpos( $value, 'E' ) !== false || strpos( $value, 'e' ) !== false; + } + + private function applyDefaultPrecision( $value ) { + return round( $value, $this->defaultPrecision ); + } + + private function getPrecisionFrom( $value ) { + return strlen( strrchr( $value, "." ) ) - 1; + } + + private function doFormatExponentialNotation( $value ) { + return str_replace( + [ '.', 'E' ], + [ $this->getSeparatorByLanguage( self::DECIMAL_SEPARATOR, self::CONTENT_LANGUAGE ), 'e' ], + $value + ); + } + + private function doFormatWithPrecision( $value, $precision = false, $decimal, $thousand ) { + + $replacement = 0; + + // Don't try to be more precise than the actual value (e.g avoid turning + // 72.769482308 into 72.76948230799999350892904) + if ( ( $actualPrecision = $this->getPrecisionFrom( $value ) ) < $precision && $actualPrecision > 0 && !$this->isScientific( $value ) ) { + $replacement = $precision - $actualPrecision; + $precision = $actualPrecision; + } + + $value = (float)$value; + $isNegative = $value < 0; + + // Format to some level of precision; number_format does rounding and + // locale formatting, x and y are used temporarily since number_format + // supports only single characters for either + $value = number_format( $value, $precision, 'x', 'y' ); + + // Due to https://bugs.php.net/bug.php?id=76824 + if ( $isNegative && $value >= 0 ) { + $value = "-$value"; + } + + $value = str_replace( + [ 'x', 'y' ], + [ + $decimal, + $thousand + ], + $value + ); + + if ( $replacement > 0 ) { + $value .= str_repeat( '0', $replacement ); + } + + return $value; + } + + private function getUserLanguage() { + + $language = Message::USER_LANGUAGE; + + // The preferred language is set when the output formatter contained + // something like LOCL@es + + if ( $this->options->has( self::PREFERRED_LANGUAGE ) && $this->options->get( self::PREFERRED_LANGUAGE ) ) { + $language = $this->options->get( self::PREFERRED_LANGUAGE ); + } elseif ( $this->options->has( self::USER_LANGUAGE ) && $this->options->get( self::USER_LANGUAGE ) ) { + $language = $this->options->get( self::USER_LANGUAGE ); + } + + return $language; + } + + private function getContentLanguage() { + + $language = Message::CONTENT_LANGUAGE; + + if ( $this->options->has( self::CONTENT_LANGUAGE ) && $this->options->get( self::CONTENT_LANGUAGE ) ) { + $language = $this->options->get( self::CONTENT_LANGUAGE ); + } + + return $language; + } + + private function getPreferredLocalizedSeparator( $custom, $standard, $language ) { + + if ( $this->options->has( $custom ) && ( $separator = $this->options->get( $custom ) ) !== false ) { + return $separator; + } + + return Message::get( $standard, Message::TEXT, $language ); + } + +} |