Completed
Push — BoundedQuantityValue ( f1c381...544562 )
by Daniel
05:59 queued 02:57
created

QuantityFormatter::formatMargin()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 27
rs 8.439
cc 6
eloc 14
nc 9
nop 2
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;
0 ignored issues
show
Unused Code introduced by
return null; does not seem to be reachable.

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 or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

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.

Loading history...
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