1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace ValueFormatters; |
4
|
|
|
|
5
|
|
|
use DataValues\DecimalMath; |
6
|
|
|
use DataValues\DecimalValue; |
7
|
|
|
use DataValues\QuantityValue; |
8
|
|
|
use DataValues\UnboundedQuantityValue; |
9
|
|
|
use InvalidArgumentException; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* Plain text formatter for quantity values. |
13
|
|
|
* |
14
|
|
|
* @since 0.1 |
15
|
|
|
* |
16
|
|
|
* @license GPL-2.0+ |
17
|
|
|
* @author Daniel Kinzler |
18
|
|
|
* @author Thiemo Mättig |
19
|
|
|
*/ |
20
|
|
|
class QuantityFormatter extends ValueFormatterBase { |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Option key for enabling or disabling output of the uncertainty margin (e.g. "+/-5"). |
24
|
|
|
* Per default, the uncertainty margin is included in the output. |
25
|
|
|
* This option must have a boolean value. |
26
|
|
|
*/ |
27
|
|
|
const OPT_SHOW_UNCERTAINTY_MARGIN = 'showQuantityUncertaintyMargin'; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Value for the OPT_SHOW_UNCERTAINTY_MARGIN indicating that the uncertainty margin |
31
|
|
|
* should not be shown. |
32
|
|
|
*/ |
33
|
|
|
const SHOW_UNCERTAINTY_MARGIN_NEVER = 'never'; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Value for the OPT_SHOW_UNCERTAINTY_MARGIN indicating that the uncertainty margin |
37
|
|
|
* should be shown if we are displaying a QuantityValue. |
38
|
|
|
*/ |
39
|
|
|
const SHOW_UNCERTAINTY_MARGIN_IF_KNOWN = 'if-known'; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Value for the OPT_SHOW_UNCERTAINTY_MARGIN indicating that the uncertainty margin |
43
|
|
|
* should be shown if we are displaying a non-exact QuantityValue. |
44
|
|
|
*/ |
45
|
|
|
const SHOW_UNCERTAINTY_MARGIN_IF_NOT_ZERO = 'if-not-zero'; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Option key for determining what level of rounding to apply to the numbers |
49
|
|
|
* included in the output. The value of this option must be an integer or a boolean. |
50
|
|
|
* |
51
|
|
|
* If an integer is given, this is the exponent of the last significant decimal digits |
52
|
|
|
* - that is, -2 would round to two digits after the decimal point, and 1 would round |
53
|
|
|
* to two digits before the decimal point. 0 would indicate rounding to integers. |
54
|
|
|
* |
55
|
|
|
* If the value is a boolean, false means no rounding at all (useful e.g. in diffs), |
56
|
|
|
* and true means automatic rounding based on what $quantity->getOrderOfUncertainty() |
57
|
|
|
* returns. |
58
|
|
|
*/ |
59
|
|
|
const OPT_APPLY_ROUNDING = 'applyRounding'; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Option key controlling whether the quantity's unit of measurement should be included |
63
|
|
|
* in the output. |
64
|
|
|
* |
65
|
|
|
* @since 0.5 |
66
|
|
|
*/ |
67
|
|
|
const OPT_APPLY_UNIT = 'applyUnit'; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var DecimalMath |
71
|
|
|
*/ |
72
|
|
|
private $decimalMath; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @var DecimalFormatter |
76
|
|
|
*/ |
77
|
|
|
private $decimalFormatter; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @var ValueFormatter|null |
81
|
|
|
*/ |
82
|
|
|
private $vocabularyUriFormatter; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @var string |
86
|
|
|
*/ |
87
|
|
|
private $quantityWithUnitFormat; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @since 0.6 |
91
|
|
|
* |
92
|
|
|
* @param FormatterOptions|null $options |
93
|
|
|
* @param DecimalFormatter|null $decimalFormatter |
94
|
|
|
* @param ValueFormatter|null $vocabularyUriFormatter |
95
|
|
|
* @param string|null $quantityWithUnitFormat Format string with two placeholders, $1 for the |
96
|
|
|
* number and $2 for the unit. Warning, this must be under the control of the application, not |
97
|
|
|
* under the control of the user, because it allows HTML injections in subclasses that return |
98
|
|
|
* HTML. |
99
|
|
|
*/ |
100
|
|
|
public function __construct( |
101
|
|
|
FormatterOptions $options = null, |
102
|
|
|
DecimalFormatter $decimalFormatter = null, |
103
|
|
|
ValueFormatter $vocabularyUriFormatter = null, |
104
|
|
|
$quantityWithUnitFormat = null |
105
|
|
|
) { |
106
|
|
|
parent::__construct( $options ); |
107
|
|
|
|
108
|
|
|
$this->defaultOption( |
109
|
|
|
self::OPT_SHOW_UNCERTAINTY_MARGIN, |
110
|
|
|
self::SHOW_UNCERTAINTY_MARGIN_IF_KNOWN |
111
|
|
|
); |
112
|
|
|
$this->defaultOption( self::OPT_APPLY_ROUNDING, true ); |
113
|
|
|
$this->defaultOption( self::OPT_APPLY_UNIT, true ); |
114
|
|
|
|
115
|
|
|
$this->decimalFormatter = $decimalFormatter ?: new DecimalFormatter( $this->options ); |
116
|
|
|
$this->vocabularyUriFormatter = $vocabularyUriFormatter; |
117
|
|
|
$this->quantityWithUnitFormat = $quantityWithUnitFormat ?: '$1 $2'; |
118
|
|
|
|
119
|
|
|
// plain composition should be sufficient |
120
|
|
|
$this->decimalMath = new DecimalMath(); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* @since 0.6 |
125
|
|
|
* |
126
|
|
|
* @return string |
127
|
|
|
*/ |
128
|
|
|
final protected function getQuantityWithUnitFormat() { |
129
|
|
|
return $this->quantityWithUnitFormat; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* @see ValueFormatter::format |
134
|
|
|
* |
135
|
|
|
* @param UnboundedQuantityValue $value |
136
|
|
|
* |
137
|
|
|
* @throws InvalidArgumentException |
138
|
|
|
* @return string Text |
139
|
|
|
*/ |
140
|
|
|
public function format( $value ) { |
141
|
|
|
if ( !( $value instanceof UnboundedQuantityValue ) ) { |
142
|
|
|
throw new InvalidArgumentException( 'Data value type mismatch. Expected a QuantityValue.' ); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
return $this->formatQuantityValue( $value ); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* @since 0.6 |
150
|
|
|
* |
151
|
|
|
* @param UnboundedQuantityValue $quantity |
152
|
|
|
* |
153
|
|
|
* @return string Text |
154
|
|
|
*/ |
155
|
|
|
protected function formatQuantityValue( UnboundedQuantityValue $quantity ) { |
156
|
|
|
$formatted = $this->formatNumber( $quantity ); |
157
|
|
|
$unit = $this->formatUnit( $quantity->getUnit() ); |
158
|
|
|
|
159
|
|
|
if ( $unit !== null ) { |
160
|
|
|
$formatted = strtr( |
161
|
|
|
$this->quantityWithUnitFormat, |
162
|
|
|
array( |
163
|
|
|
'$1' => $formatted, |
164
|
|
|
'$2' => $unit |
165
|
|
|
) |
166
|
|
|
); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
return $formatted; |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* @since 0.6 |
174
|
|
|
* |
175
|
|
|
* @param UnboundedQuantityValue $quantity |
176
|
|
|
* |
177
|
|
|
* @return string Text |
178
|
|
|
*/ |
179
|
|
|
protected function formatNumber( UnboundedQuantityValue $quantity ) { |
180
|
|
|
$roundingMode = $this->options->getOption( self::OPT_APPLY_ROUNDING ); |
181
|
|
|
$roundingExponent = $this->getRoundingExponent( $quantity, $roundingMode ); |
182
|
|
|
|
183
|
|
|
$amount = $quantity->getAmount(); |
184
|
|
|
$roundedAmount = $this->decimalMath->roundToExponent( $amount, $roundingExponent ); |
185
|
|
|
$formatted = $this->decimalFormatter->format( $roundedAmount ); |
186
|
|
|
|
187
|
|
|
if ( $quantity instanceof QuantityValue ) { |
188
|
|
|
// TODO: strip trailing zeros from margin |
189
|
|
|
$margin = $this->formatMargin( $quantity->getUncertaintyMargin(), $roundingExponent ); |
190
|
|
|
if ( $margin !== null ) { |
191
|
|
|
// TODO: use localizable pattern for constructing the output. |
192
|
|
|
$formatted .= '±' . $margin; |
193
|
|
|
} |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
return $formatted; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Returns the rounding exponent based on the given $quantity. |
201
|
|
|
* |
202
|
|
|
* @param UnboundedQuantityValue $quantity |
203
|
|
|
* @param bool|int $roundingMode The rounding exponent, or true for rounding to the order of |
204
|
|
|
* uncertainty (significant digits) of a QuantityValue, or false to not apply |
205
|
|
|
* rounding (that is, round to all digits). |
206
|
|
|
* |
207
|
|
|
* @return int |
208
|
|
|
*/ |
209
|
|
|
private function getRoundingExponent( UnboundedQuantityValue $quantity, $roundingMode ) { |
210
|
|
|
if ( is_int( $roundingMode ) ) { |
211
|
|
|
// round to the given exponent |
212
|
|
|
return $roundingMode; |
213
|
|
|
} elseif ( $roundingMode && ( $quantity instanceof QuantityValue ) ) { |
214
|
|
|
// round to the order of uncertainty (QuantityValue only) |
215
|
|
|
return $quantity->getOrderOfUncertainty(); |
216
|
|
|
} else { |
217
|
|
|
// to keep all digits, use the negative length of the fractional part |
218
|
|
|
return -strlen( $quantity->getAmount()->getFractionalPart() ); |
219
|
|
|
} |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* @param DecimalValue $margin |
224
|
|
|
* @param int $roundingExponent |
225
|
|
|
* |
226
|
|
|
* @return string|null Text |
227
|
|
|
*/ |
228
|
|
|
private function formatMargin( DecimalValue $margin, $roundingExponent ) { |
229
|
|
|
$marginMode = $this->options->getOption( self::OPT_SHOW_UNCERTAINTY_MARGIN ); |
230
|
|
|
|
231
|
|
|
// map legacy option values |
232
|
|
|
if ( $marginMode === true ) { |
233
|
|
|
// old default behavior |
234
|
|
|
$marginMode = self::SHOW_UNCERTAINTY_MARGIN_IF_NOT_ZERO; |
235
|
|
|
} elseif ( $marginMode === false ) { |
236
|
|
|
$marginMode = self::SHOW_UNCERTAINTY_MARGIN_NEVER; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
if ( $marginMode === self::SHOW_UNCERTAINTY_MARGIN_NEVER ) { |
240
|
|
|
return null; |
241
|
|
|
return null; |
|
|
|
|
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
// TODO: never round to 0! See bug #56892 |
245
|
|
|
$roundedMargin = $this->decimalMath->roundToExponent( $margin, $roundingExponent ); |
246
|
|
|
|
247
|
|
|
if ( $marginMode === self::SHOW_UNCERTAINTY_MARGIN_IF_NOT_ZERO |
248
|
|
|
&& $roundedMargin->isZero() |
249
|
|
|
) { |
250
|
|
|
return null; |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
return $this->decimalFormatter->format( $roundedMargin ); |
254
|
|
|
} |
255
|
|
|
|
256
|
|
|
/** |
257
|
|
|
* @since 0.6 |
258
|
|
|
* |
259
|
|
|
* @param string $unit URI |
260
|
|
|
* |
261
|
|
|
* @return string|null Text |
262
|
|
|
*/ |
263
|
|
|
protected function formatUnit( $unit ) { |
264
|
|
|
if ( $this->vocabularyUriFormatter === null |
265
|
|
|
|| !$this->options->getOption( self::OPT_APPLY_UNIT ) |
266
|
|
|
|| $unit === '' |
267
|
|
|
|| $unit === '1' |
268
|
|
|
) { |
269
|
|
|
return null; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
return $this->vocabularyUriFormatter->format( $unit ); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
} |
276
|
|
|
|
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.