value, mapping a normalized unit to the * converted value. Used for conversion tooltips. * @var array */ protected $m_unitvalues; /** * Whether the unit is preferred as prefix or not * * @var array */ protected $prefixalUnitPreference = []; /** * Canonical identifier for the unit that the user gave as input. Used * to avoid printing this in conversion tooltips again. If the * outputformat was set to show another unit, then the values of * $m_caption and $m_unitin will be updated as if the formatted string * had been the original user input, i.e. the two values reflect what * is currently printed. * @var string */ protected $m_unitin; /** * @var integer|null */ protected $precision = null; /** * @var IntlNumberFormatter */ private $intlNumberFormatter; /** * @var ValueFormatter */ private $valueFormatter; /** * @since 2.4 * * @param string $typeid */ public function __construct( $typeid = '' ) { parent::__construct( $typeid ); $this->intlNumberFormatter = IntlNumberFormatter::getInstance(); $this->intlNumberFormatter->reset(); } /** * Parse a string of the form "number unit" where unit is optional. The * results are stored in the $number and $unit parameters. Returns an * error code. * @param $value string to parse * @param $number call-by-ref parameter that will be set to the numerical value * @param $unit call-by-ref parameter that will be set to the "unit" string (after the number) * @return integer 0 (no errors), 1 (no number found at all), 2 (number * too large for this platform) */ public function parseNumberValue( $value, &$number, &$unit, &$asPrefix = false ) { $intlNumberFormatter = $this->getNumberFormatter(); // Parse to find $number and (possibly) $unit $kiloseparator = $intlNumberFormatter->getSeparatorByLanguage( self::THOUSANDS_SEPARATOR, IntlNumberFormatter::CONTENT_LANGUAGE ); $decseparator = $intlNumberFormatter->getSeparatorByLanguage( self::DECIMAL_SEPARATOR, IntlNumberFormatter::CONTENT_LANGUAGE ); // #753 $regex = '/([-+]?\s*(?:' . // Either numbers like 10,000.99 that start with a digit '\d+(?:\\' . $kiloseparator . '\d\d\d)*(?:\\' . $decseparator . '\d+)?' . // or numbers like .001 that start with the decimal separator '|\\' . $decseparator . '\d+' . ')\s*(?:[eE][-+]?\d+)?)/u'; // #1718 Whether to preserve spaces in unit labels or not (e.g. sq mi, sqmi) $space = $this->isEnabledFeature( SMW_DV_NUMV_USPACE ) ? ' ' : ''; $parts = preg_split( $regex, trim( str_replace( [ ' ', ' ', ' ', ' ' ], $space, $value ) ), 2, PREG_SPLIT_DELIM_CAPTURE ); if ( count( $parts ) >= 2 ) { $numstring = str_replace( $kiloseparator, '', preg_replace( '/\s*/u', '', $parts[1] ) ); // simplify if ( $decseparator != '.' ) { $numstring = str_replace( $decseparator, '.', $numstring ); } list( $number ) = sscanf( $numstring, "%f" ); if ( count( $parts ) >= 3 ) { $asPrefix = $parts[0] !== ''; $unit = $this->normalizeUnit( $parts[0] !== '' ? $parts[0] : $parts[2] ); } } if ( ( count( $parts ) == 1 ) || ( $numstring === '' ) ) { // no number found return 1; } elseif ( is_infinite( $number ) ) { // number is too large for this platform return 2; } else { return 0; } } /** * @see DataValue::parseUserValue */ protected function parseUserValue( $value ) { // Set caption if ( $this->m_caption === false ) { $this->m_caption = $value; } if ( $value !== '' && $value{0} === ':' ) { $this->addErrorMsg( [ 'smw-datavalue-invalid-number', $value ] ); return; } $this->m_unitin = false; $this->m_unitvalues = false; $number = $unit = ''; $error = $this->parseNumberValue( $value, $number, $unit ); if ( $error == 1 ) { // no number found $this->addErrorMsg( [ 'smw_nofloat', $value ] ); } elseif ( $error == 2 ) { // number is too large for this platform $this->addErrorMsg( [ 'smw_infinite', $value ] ); } elseif ( $this->getTypeID() === '_num' && $unit !== '' ) { $this->addErrorMsg( [ 'smw-datavalue-number-textnotallowed', $unit, $number ] ); } elseif ( $number === null ) { $this->addErrorMsg( [ 'smw-datavalue-number-nullnotallowed', $value ] ); // #1628 } elseif ( $this->convertToMainUnit( $number, $unit ) === false ) { // so far so good: now convert unit and check if it is allowed $this->addErrorMsg( [ 'smw_unitnotallowed', $unit ] ); } // note that convertToMainUnit() also sets m_dataitem if valid } /** * @see SMWDataValue::loadDataItem() * @param $dataitem SMWDataItem * @return boolean */ protected function loadDataItem( SMWDataItem $dataItem ) { if ( $dataItem->getDIType() !== SMWDataItem::TYPE_NUMBER ) { return false; } $this->m_dataitem = $dataItem; $this->m_caption = false; $this->m_unitin = false; $this->makeUserValue(); $this->m_unitvalues = false; return true; } /** * @see DataValue::setOutputFormat * * @param $string $formatstring */ public function setOutputFormat( $formatstring ) { if ( $formatstring == $this->m_outformat ) { return null; } // #1591 $this->findPreferredLanguageFrom( $formatstring ); // #1335 $this->m_outformat = $this->findPrecisionFrom( $formatstring ); if ( $this->isValid() ) { // update caption/unitin for this format $this->m_caption = false; $this->m_unitin = false; $this->makeUserValue(); } } /** * @since 1.6 * * @return float */ public function getNumber() { if ( !$this->isValid() ) { return 999999999999999; } return $this->m_dataitem->getNumber(); } /** * @since 2.4 * * @return float */ public function getLocalizedFormattedNumber( $value ) { return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision() ); } /** * @since 2.4 * * @return float */ public function getNormalizedFormattedNumber( $value ) { return $this->getNumberFormatter()->format( $value, $this->getPreferredDisplayPrecision(), IntlNumberFormatter::VALUE_FORMAT ); } /** * @see DataValue::getShortWikiText * * @return string */ public function getShortWikiText( $linker = null ) { if ( $this->valueFormatter === null ) { $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); } $this->valueFormatter->setDataValue( $this ); return $this->valueFormatter->format( DataValueFormatter::WIKI_SHORT, $linker ); } /** * @see DataValue::getShortHTMLText * * @return string */ public function getShortHTMLText( $linker = null ) { if ( $this->valueFormatter === null ) { $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); } $this->valueFormatter->setDataValue( $this ); return $this->valueFormatter->format( DataValueFormatter::HTML_SHORT, $linker ); } /** * @see DataValue::getLongWikiText * * @return string */ public function getLongWikiText( $linker = null ) { if ( $this->valueFormatter === null ) { $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); } $this->valueFormatter->setDataValue( $this ); return $this->valueFormatter->format( DataValueFormatter::WIKI_LONG, $linker ); } /** * @see DataValue::getLongHTMLText * * @return string */ public function getLongHTMLText( $linker = null ) { if ( $this->valueFormatter === null ) { $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); } $this->valueFormatter->setDataValue( $this ); return $this->valueFormatter->format( DataValueFormatter::HTML_LONG, $linker ); } /** * @see DataValue::getWikiValue * * @return string */ public function getWikiValue() { if ( $this->valueFormatter === null ) { $this->valueFormatter = $this->dataValueServiceFactory->getValueFormatter( $this ); } $this->valueFormatter->setDataValue( $this ); return $this->valueFormatter->format( DataValueFormatter::VALUE ); } /** * @see DataVelue::getInfolinks * * @return array */ public function getInfolinks() { // When generating an infoLink, use the normalized value without any // precision limitation $this->setOption( self::NO_DISP_PRECISION_LIMIT, true ); $this->setOption( self::OPT_CONTENT_LANGUAGE, Message::CONTENT_LANGUAGE ); $infoLinks = parent::getInfolinks(); $this->setOption( self::NO_DISP_PRECISION_LIMIT, false ); return $infoLinks; } /** * @since 2.4 * * @return string */ public function getCanonicalMainUnit() { return $this->m_unitin; } /** * Returns array of converted unit-value-pairs that can be * printed. * * @since 2.4 * * @return array */ public function getConvertedUnitValues() { $this->makeConversionValues(); return $this->m_unitvalues; } /** * Return the unit in which the returned value is to be interpreted. * This string is a plain UTF-8 string without wiki or html markup. * The returned value is a canonical ID for the main unit. * Returns the empty string if no unit is given for the value. * Overwritten by subclasses that support units. */ public function getUnit() { return ''; } /** * @since 2.4 * * @param string $unit * * @return boolean */ public function hasPrefixalUnitPreference( $unit ) { return isset( $this->prefixalUnitPreference[$unit] ) && $this->prefixalUnitPreference[$unit]; } /** * Create links to mapping services based on a wiki-editable message. * The parameters available to the message are: * $1: string of numerical value in English punctuation * $2: string of integer version of value, in English punctuation * * @return array */ protected function getServiceLinkParams() { if ( $this->isValid() ) { return [ strval( $this->m_dataitem->getNumber() ), strval( round( $this->m_dataitem->getNumber() ) ) ]; } else { return []; } } /** * Transform a (typically unit-) string into a normalised form, * so that, e.g., "km²" and "km2" do not need to be * distinguished. */ public function normalizeUnit( $unit ) { $unit = str_replace( [ '[[', ']]' ], '', trim( $unit ) ); // allow simple links to be used inside annotations $unit = str_replace( [ '²', '2' ], '²', $unit ); $unit = str_replace( [ '³', '3' ], '³', $unit ); return smwfXMLContentEncode( $unit ); } /** * Compute the value based on the given input number and unit string. * If the unit is not supported, return false, otherwise return true. * This is called when parsing user input, where the given unit value * has already been normalized. * * This class does not support any (non-empty) units, but subclasses * may overwrite this behavior. * @param $number float value obtained by parsing user input * @param $unit string after the numericla user input * @return boolean specifying if the unit string is allowed */ protected function convertToMainUnit( $number, $unit ) { $this->m_dataitem = new SMWDINumber( $number ); $this->m_unitin = ''; return ( $unit === '' ); } /** * This method creates an array of unit-value-pairs that should be * printed. Units are the keys and should be canonical unit IDs. * The result is stored in $this->m_unitvalues. Again, any class that * requires effort for doing this should first check whether the array * is already set (i.e. not false) before doing any work. * Note that the values should be plain numbers. Output formatting is done * later when needed. Also, it should be checked if the value is valid * before trying to calculate with its contents. * This method also must call or implement convertToMainUnit(). * * Overwritten by subclasses that support units. */ protected function makeConversionValues() { $this->m_unitvalues = [ '' => $this->m_dataitem->getNumber() ]; } /** * This method is used when no user input was given to find the best * values for m_unitin and m_caption. After conversion, * these fields will look as if they were generated from user input, * and convertToMainUnit() will have been called (if not, it would be * blocked by the presence of m_unitin). * * Overwritten by subclasses that support units. */ protected function makeUserValue() { $this->m_caption = ''; $number = $this->m_dataitem->getNumber(); // -u is the format for displaying the unit only if ( $this->m_outformat == '-u' ) { $this->m_caption = ''; } elseif ( ( $this->m_outformat != '-' ) && ( $this->m_outformat != '-n' ) ) { $this->m_caption = $this->getLocalizedFormattedNumber( $number ); } else { $this->m_caption = $this->getNormalizedFormattedNumber( $number ); } // no unit ever, so nothing to do about this $this->m_unitin = ''; } /** * Return an array of major unit strings (ids only recommended) supported by * this datavalue. * * Overwritten by subclasses that support units. */ public function getUnitList() { return [ '' ]; } protected function getPreferredDisplayPrecision() { // Don't restrict the value with a display precision if ( $this->getProperty() === null || $this->getOption( self::NO_DISP_PRECISION_LIMIT ) ) { return false; } if ( $this->precision === null ) { $this->precision = $this->dataValueServiceFactory->getPropertySpecificationLookup()->getDisplayPrecision( $this->getProperty() ); } return $this->precision; } private function findPrecisionFrom( $formatstring ) { if ( strpos( $formatstring, '-' ) === false ) { return $formatstring; } $parts = explode( '-', $formatstring ); // Find precision from annotated -p formatstring which // has priority over a possible _PREC value foreach ( $parts as $key => $value ) { if ( strpos( $value, 'p' ) !== false && is_numeric( substr( $value, 1 ) ) ) { $this->precision = strval( substr( $value, 1 ) ); unset( $parts[$key] ); } } // Rebuild formatstring without a possible p element to ensure other // options can be used in combination such as -n-p2 etc. return implode( '-', $parts ); } private function getNumberFormatter() { $this->intlNumberFormatter->setOption( IntlNumberFormatter::USER_LANGUAGE, $this->getOption( self::OPT_USER_LANGUAGE ) ); $this->intlNumberFormatter->setOption( IntlNumberFormatter::CONTENT_LANGUAGE, $this->getOption( self::OPT_CONTENT_LANGUAGE ) ); $this->intlNumberFormatter->setOption( self::THOUSANDS_SEPARATOR, $this->getOption( self::THOUSANDS_SEPARATOR ) ); $this->intlNumberFormatter->setOption( self::DECIMAL_SEPARATOR, $this->getOption( self::DECIMAL_SEPARATOR ) ); return $this->intlNumberFormatter; } private function findPreferredLanguageFrom( &$formatstring ) { // Localized preferred user language if ( strpos( $formatstring, 'LOCL' ) !== false && ( $languageCode = Localizer::getLanguageCodeFrom( $formatstring ) ) !== false ) { $this->intlNumberFormatter->setOption( IntlNumberFormatter::PREFERRED_LANGUAGE, $languageCode ); } // Remove any remaining $formatstring = str_replace( [ '#LOCL', 'LOCL' ], '', $formatstring ); } }