DataValues /
Number
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
| 1 | <?php |
||
| 2 | |||
| 3 | namespace ValueParsers; |
||
| 4 | |||
| 5 | use DataValues\DecimalMath; |
||
| 6 | use DataValues\DecimalValue; |
||
| 7 | use DataValues\IllegalValueException; |
||
| 8 | use DataValues\QuantityValue; |
||
| 9 | use InvalidArgumentException; |
||
| 10 | |||
| 11 | /** |
||
| 12 | * ValueParser that parses the string representation of a quantity. |
||
| 13 | * |
||
| 14 | * @since 0.1 |
||
| 15 | * |
||
| 16 | * @license GPL-2.0+ |
||
| 17 | * @author Daniel Kinzler |
||
| 18 | */ |
||
| 19 | class QuantityParser extends StringValueParser { |
||
| 20 | |||
| 21 | const FORMAT_NAME = 'quantity'; |
||
| 22 | |||
| 23 | /** |
||
| 24 | * The unit of the value to parse. If this option is given, it's illegal to also specify |
||
| 25 | * a unit in the input string. |
||
| 26 | * |
||
| 27 | * @since 0.5 |
||
| 28 | */ |
||
| 29 | const OPT_UNIT = 'unit'; |
||
| 30 | |||
| 31 | /** |
||
| 32 | * @var DecimalParser |
||
| 33 | */ |
||
| 34 | private $decimalParser; |
||
| 35 | |||
| 36 | /** |
||
| 37 | * @var NumberUnlocalizer |
||
| 38 | */ |
||
| 39 | private $unlocalizer; |
||
| 40 | |||
| 41 | /** |
||
| 42 | * @since 0.1 |
||
| 43 | * |
||
| 44 | * @param ParserOptions|null $options |
||
| 45 | * @param NumberUnlocalizer|null $unlocalizer |
||
| 46 | */ |
||
| 47 | public function __construct( ParserOptions $options = null, NumberUnlocalizer $unlocalizer = null ) { |
||
| 48 | parent::__construct( $options ); |
||
| 49 | |||
| 50 | $this->defaultOption( self::OPT_UNIT, null ); |
||
| 51 | |||
| 52 | $this->unlocalizer = $unlocalizer ?: new BasicNumberUnlocalizer(); |
||
| 53 | $this->decimalParser = new DecimalParser( $this->options, $this->unlocalizer ); |
||
| 54 | } |
||
| 55 | |||
| 56 | /** |
||
| 57 | * @see StringValueParser::stringParse |
||
| 58 | * |
||
| 59 | * @since 0.1 |
||
| 60 | * |
||
| 61 | * @param string $value |
||
| 62 | * |
||
| 63 | * @return QuantityValue |
||
| 64 | * @throws ParseException |
||
| 65 | */ |
||
| 66 | protected function stringParse( $value ) { |
||
| 67 | list( $amount, $exactness, $margin, $unit ) = $this->splitQuantityString( $value ); |
||
| 68 | |||
| 69 | $unitOption = $this->getUnitFromOptions(); |
||
| 70 | |||
| 71 | if ( $unit === null ) { |
||
| 72 | $unit = $unitOption !== null ? $unitOption : '1'; |
||
| 73 | } elseif ( $unitOption !== null && $unit !== $unitOption ) { |
||
| 74 | throw new ParseException( 'Cannot specify a unit in input if a unit was fixed via options.' ); |
||
| 75 | } |
||
| 76 | |||
| 77 | try { |
||
| 78 | $quantity = $this->newQuantityFromParts( $amount, $exactness, $margin, $unit ); |
||
| 79 | return $quantity; |
||
| 80 | } catch ( IllegalValueException $ex ) { |
||
| 81 | throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME ); |
||
| 82 | } |
||
| 83 | } |
||
| 84 | |||
| 85 | /** |
||
| 86 | * @return string|null |
||
| 87 | */ |
||
| 88 | private function getUnitFromOptions() { |
||
| 89 | $unit = $this->getOption( self::OPT_UNIT ); |
||
| 90 | return $unit === null ? null : trim( $unit ); |
||
| 91 | } |
||
| 92 | |||
| 93 | /** |
||
| 94 | * Constructs a QuantityValue from the given parts. |
||
| 95 | * |
||
| 96 | * @see splitQuantityString |
||
| 97 | * |
||
| 98 | * @param string $amount decimal representation of the amount |
||
| 99 | * @param string|null $exactness either '!' to indicate an exact value, |
||
| 100 | * or '~' for "automatic", or null if $margin should be used. |
||
| 101 | * @param string|null $margin decimal representation of the uncertainty margin |
||
| 102 | * @param string $unit the unit identifier (use "1" for unitless quantities). |
||
| 103 | * |
||
| 104 | * @throws ParseException if one of the decimals could not be parsed. |
||
| 105 | * @throws IllegalValueException if the QuantityValue could not be constructed |
||
| 106 | * @return QuantityValue |
||
| 107 | */ |
||
| 108 | private function newQuantityFromParts( $amount, $exactness, $margin, $unit ) { |
||
| 109 | list( $amount, $exponent ) = $this->decimalParser->splitDecimalExponent( $amount ); |
||
| 110 | $amountValue = $this->decimalParser->parse( $amount ); |
||
| 111 | |||
| 112 | if ( $exactness === '!' ) { |
||
| 113 | // the amount is an exact number |
||
| 114 | $amountValue = $this->decimalParser->applyDecimalExponent( $amountValue, $exponent ); |
||
| 115 | $quantity = $this->newExactQuantity( $amountValue, $unit ); |
||
| 116 | } elseif ( $margin !== null ) { |
||
| 117 | // uncertainty margin given |
||
| 118 | // NOTE: the pattern for scientific notation is 2e3 +/- 1e2, so the exponents are treated separately. |
||
| 119 | $marginValue = $this->decimalParser->parse( $margin ); |
||
| 120 | $amountValue = $this->decimalParser->applyDecimalExponent( $amountValue, $exponent ); |
||
| 121 | $quantity = $this->newUncertainQuantityFromMargin( $amountValue, $unit, $marginValue ); |
||
| 122 | } elseif ( $exactness === '~' ) { |
||
| 123 | // derive uncertainty from given decimals |
||
| 124 | // NOTE: with scientific notation, the exponent applies to the uncertainty bounds, too |
||
| 125 | $quantity = $this->newUncertainQuantityFromDigits( $amountValue, $unit, $exponent ); |
||
| 126 | } else { |
||
| 127 | $amountValue = $this->decimalParser->applyDecimalExponent( $amountValue, $exponent ); |
||
| 128 | return new QuantityValue( $amountValue, $unit, null, null ); |
||
|
0 ignored issues
–
show
|
|||
| 129 | } |
||
| 130 | |||
| 131 | return $quantity; |
||
| 132 | } |
||
| 133 | |||
| 134 | /** |
||
| 135 | * Splits a quantity string into its syntactic parts. |
||
| 136 | * |
||
| 137 | * @see newQuantityFromParts |
||
| 138 | * |
||
| 139 | * @param string $value |
||
| 140 | * |
||
| 141 | * @throws InvalidArgumentException If $value is not a string |
||
| 142 | * @throws ParseException If $value does not match the expected pattern |
||
| 143 | * @return array list( $amount, $exactness, $margin, $unit ). |
||
| 144 | * Parts not present in $value will be null |
||
| 145 | */ |
||
| 146 | private function splitQuantityString( $value ) { |
||
| 147 | if ( !is_string( $value ) ) { |
||
| 148 | throw new InvalidArgumentException( '$value must be a string' ); |
||
| 149 | } |
||
| 150 | |||
| 151 | //TODO: allow explicitly specifying the number of significant figures |
||
| 152 | //TODO: allow explicitly specifying the uncertainty interval |
||
| 153 | |||
| 154 | $numberPattern = $this->unlocalizer->getNumberRegex( '@' ); |
||
| 155 | $unitPattern = $this->unlocalizer->getUnitRegex( '@' ); |
||
| 156 | |||
| 157 | $pattern = '@^' |
||
| 158 | . '\s*(' . $numberPattern . ')' // $1: amount |
||
| 159 | . '\s*(?:' |
||
| 160 | . '([~!])' // $2: '!' for "exact", '~' for "approx", or nothing |
||
| 161 | . '|(?:\+/?-|±)\s*(' . $numberPattern . ')' // $3: plus/minus offset (uncertainty margin) |
||
| 162 | . '|' // or nothing |
||
| 163 | . ')' |
||
| 164 | . '\s*(' . $unitPattern . ')?' // $4: unit |
||
| 165 | . '\s*$@u'; |
||
| 166 | |||
| 167 | if ( !preg_match( $pattern, $value, $groups ) ) { |
||
| 168 | throw new ParseException( 'Malformed quantity', $value, self::FORMAT_NAME ); |
||
| 169 | } |
||
| 170 | |||
| 171 | // Remove $0. |
||
| 172 | array_shift( $groups ); |
||
| 173 | |||
| 174 | array_walk( $groups, function( &$element ) { |
||
| 175 | if ( $element === '' ) { |
||
| 176 | $element = null; |
||
| 177 | } |
||
| 178 | } ); |
||
| 179 | |||
| 180 | return array_pad( $groups, 4, null ); |
||
| 181 | } |
||
| 182 | |||
| 183 | /** |
||
| 184 | * Returns a QuantityValue representing the given amount. |
||
| 185 | * The amount is assumed to be absolutely exact, that is, |
||
| 186 | * the upper and lower bound will be the same as the amount. |
||
| 187 | * |
||
| 188 | * @param DecimalValue $amount |
||
| 189 | * @param string $unit The quantity's unit (use "1" for unit-less quantities) |
||
| 190 | * |
||
| 191 | * @return QuantityValue |
||
| 192 | */ |
||
| 193 | private function newExactQuantity( DecimalValue $amount, $unit = '1' ) { |
||
| 194 | $lowerBound = $amount; |
||
| 195 | $upperBound = $amount; |
||
| 196 | |||
| 197 | return new QuantityValue( $amount, $unit, $upperBound, $lowerBound ); |
||
| 198 | } |
||
| 199 | |||
| 200 | /** |
||
| 201 | * Returns a QuantityValue representing the given amount, automatically assuming |
||
| 202 | * a level of uncertainty based on the digits given. |
||
| 203 | * |
||
| 204 | * The upper and lower bounds are determined automatically from the given |
||
| 205 | * digits by increasing resp. decreasing the least significant digit. |
||
| 206 | * E.g. "+0.01" would have upperBound "+0.02" and lowerBound "+0.01", |
||
| 207 | * while "-100" would have upperBound "-99" and lowerBound "-101". |
||
| 208 | * |
||
| 209 | * @param DecimalValue $amount The quantity |
||
| 210 | * @param string $unit The quantity's unit (use "1" for unit-less quantities) |
||
| 211 | * @param DecimalValue $margin |
||
| 212 | * |
||
| 213 | * @return QuantityValue |
||
| 214 | */ |
||
| 215 | private function newUncertainQuantityFromMargin( DecimalValue $amount, $unit = '1', DecimalValue $margin ) { |
||
| 216 | $decimalMath = new DecimalMath(); |
||
| 217 | $margin = $margin->computeAbsolute(); |
||
| 218 | |||
| 219 | $lowerBound = $decimalMath->sum( $amount, $margin->computeComplement() ); |
||
| 220 | $upperBound = $decimalMath->sum( $amount, $margin ); |
||
| 221 | |||
| 222 | return new QuantityValue( $amount, $unit, $upperBound, $lowerBound ); |
||
| 223 | } |
||
| 224 | |||
| 225 | /** |
||
| 226 | * Returns a QuantityValue representing the given amount, automatically assuming |
||
| 227 | * a level of uncertainty based on the digits given. |
||
| 228 | * |
||
| 229 | * The upper and lower bounds are determined automatically from the given |
||
| 230 | * digits by increasing resp. decreasing the least significant digit. |
||
| 231 | * E.g. "+0.01" would have upperBound "+0.02" and lowerBound "+0.01", |
||
| 232 | * while "-100" would have upperBound "-99" and lowerBound "-101". |
||
| 233 | * |
||
| 234 | * @param DecimalValue $amount The quantity |
||
| 235 | * @param string $unit The quantity's unit (use "1" for unit-less quantities) |
||
| 236 | * @param int $exponent Decimal exponent to apply |
||
| 237 | * |
||
| 238 | * @return QuantityValue |
||
| 239 | */ |
||
| 240 | private function newUncertainQuantityFromDigits( DecimalValue $amount, $unit = '1', $exponent = 0 ) { |
||
| 241 | $math = new DecimalMath(); |
||
| 242 | |||
| 243 | if ( $amount->getSign() === '+' ) { |
||
| 244 | $upperBound = $math->bump( $amount ); |
||
| 245 | $lowerBound = $math->slump( $amount ); |
||
| 246 | } else { |
||
| 247 | $upperBound = $math->slump( $amount ); |
||
| 248 | $lowerBound = $math->bump( $amount ); |
||
| 249 | } |
||
| 250 | |||
| 251 | $amount = $this->decimalParser->applyDecimalExponent( $amount, $exponent ); |
||
| 252 | $lowerBound = $this->decimalParser->applyDecimalExponent( $lowerBound, $exponent ); |
||
| 253 | $upperBound = $this->decimalParser->applyDecimalExponent( $upperBound, $exponent ); |
||
| 254 | |||
| 255 | return new QuantityValue( $amount, $unit, $upperBound, $lowerBound ); |
||
| 256 | } |
||
| 257 | |||
| 258 | } |
||
| 259 |
It seems like the type of the argument is not accepted by the function/method which you are calling.
In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.
We suggest to add an explicit type cast like in the following example: