Test Failed
Pull Request — master (#79)
by no
02:51 queued 26s
created

QuantityFormatter::formatUnboundedQuantityValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 2
eloc 6
nc 2
nop 1
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
	 * Option key for determining what level of rounding to apply to the numbers
31
	 * included in the output. The value of this option must be an integer or a boolean.
32
	 *
33
	 * If an integer is given, this is the exponent of the last significant decimal digits
34
	 * - that is, -2 would round to two digits after the decimal point, and 1 would round
35
	 * to two digits before the decimal point. 0 would indicate rounding to integers.
36
	 *
37
	 * If the value is a boolean, false means no rounding at all (useful e.g. in diffs),
38
	 * and true means automatic rounding based on what $quantity->getOrderOfUncertainty()
39
	 * returns.
40
	 */
41
	const OPT_APPLY_ROUNDING = 'applyRounding';
42
43
	/**
44
	 * Option key controlling whether the quantity's unit of measurement should be included
45
	 * in the output.
46
	 *
47
	 * @since 0.5
48
	 */
49
	const OPT_APPLY_UNIT = 'applyUnit';
50
51
	/**
52
	 * @var DecimalMath
53
	 */
54
	private $decimalMath;
55
56
	/**
57
	 * @var DecimalFormatter
58
	 */
59
	private $decimalFormatter;
60
61
	/**
62
	 * @var ValueFormatter|null
63
	 */
64
	private $vocabularyUriFormatter;
65
66
	/**
67
	 * @var string
68
	 */
69
	private $quantityWithUnitFormat;
70
71
	/**
72
	 * @since 0.6
73
	 *
74
	 * @param FormatterOptions|null $options
75
	 * @param DecimalFormatter|null $decimalFormatter
76
	 * @param ValueFormatter|null $vocabularyUriFormatter
77
	 * @param string|null $quantityWithUnitFormat Format string with two placeholders, $1 for the
78
	 * number and $2 for the unit. Warning, this must be under the control of the application, not
79
	 * under the control of the user, because it allows HTML injections in subclasses that return
80
	 * HTML.
81
	 */
82
	public function __construct(
83
		FormatterOptions $options = null,
84
		DecimalFormatter $decimalFormatter = null,
85
		ValueFormatter $vocabularyUriFormatter = null,
86
		$quantityWithUnitFormat = null
87
	) {
88
		parent::__construct( $options );
89
90
		$this->defaultOption( self::OPT_SHOW_UNCERTAINTY_MARGIN, true );
91
		$this->defaultOption( self::OPT_APPLY_ROUNDING, true );
92
		$this->defaultOption( self::OPT_APPLY_UNIT, true );
93
94
		$this->decimalFormatter = $decimalFormatter ?: new DecimalFormatter( $this->options );
95
		$this->vocabularyUriFormatter = $vocabularyUriFormatter;
96
		$this->quantityWithUnitFormat = $quantityWithUnitFormat ?: '$1 $2';
97
98
		// plain composition should be sufficient
99
		$this->decimalMath = new DecimalMath();
100
	}
101
102
	/**
103
	 * @since 0.6
104
	 *
105
	 * @return string
106
	 */
107
	final protected function getQuantityWithUnitFormat() {
108
		return $this->quantityWithUnitFormat;
109
	}
110
111
	/**
112
	 * @see ValueFormatter::format
113
	 *
114
	 * @param UnboundedQuantityValue|QuantityValue $value
115
	 *
116
	 * @throws InvalidArgumentException
117
	 * @return string Text
118
	 */
119
	public function format( $value ) {
120
		if ( !( $value instanceof UnboundedQuantityValue ) ) {
121
			throw new InvalidArgumentException( 'Data value type mismatch. Expected a UnboundedQuantityValue.' );
122
		}
123
124
		return $this->formatQuantityValue( $value );
125
	}
126
127
	/**
128
	 * @since 0.6
129
	 *
130
	 * @param UnboundedQuantityValue|QuantityValue $quantity
131
	 *
132
	 * @return string Text
133
	 */
134
	protected function formatQuantityValue( UnboundedQuantityValue $quantity ) {
135
		$formatted = $quantity instanceof QuantityValue
136
			? $this->formatNumber( $quantity )
137
			: $this->formatUnboundedQuantityValue( $quantity );
138
		$unit = $this->formatUnit( $quantity->getUnit() );
139
140
		if ( $unit !== null ) {
141
			$formatted = strtr(
142
				$this->quantityWithUnitFormat,
143
				array(
144
					'$1' => $formatted,
145
					'$2' => $unit
146
				)
147
			);
148
		}
149
150
		return $formatted;
151
	}
152
153
	/**
154
	 * @param UnboundedQuantityValue $quantity
155
	 *
156
	 * @return string
157
	 */
158
	protected function formatUnboundedQuantityValue( UnboundedQuantityValue $quantity ) {
159
		$amount = $quantity->getAmount();
160
		$roundingExponent = $this->options->getOption( self::OPT_APPLY_ROUNDING );
161
162
		if ( !is_bool( $roundingExponent ) ) {
163
			$amount = $this->decimalMath->roundToExponent( $amount, (int)$roundingExponent );
164
		}
165
166
		return $this->decimalFormatter->format( $amount );
167
	}
168
169
	/**
170
	 * @since 0.6
171
	 *
172
	 * @param QuantityValue $quantity
173
	 *
174
	 * @return string Text
175
	 */
176
	protected function formatNumber( QuantityValue $quantity ) {
177
		$roundingExponent = $this->getRoundingExponent( $quantity );
178
179
		$amount = $quantity->getAmount();
180
181
		if ( $roundingExponent === null ) {
182
			$formatted = $this->formatMinimalDecimal( $amount );
183
			$margin = $quantity->getUncertaintyMargin();
184
			$margin = $margin->isZero() ? null : $this->formatMinimalDecimal( $margin );
185
		} else {
186
			$roundedAmount = $this->decimalMath->roundToExponent( $amount, $roundingExponent );
187
			$formatted = $this->decimalFormatter->format( $roundedAmount );
188
			$margin = $this->formatMargin( $quantity->getUncertaintyMargin(), $roundingExponent );
189
		}
190
191
		if ( $margin !== null ) {
192
			// TODO: use localizable pattern for constructing the output.
193
			$formatted .= '±' . $margin;
194
		}
195
196
		return $formatted;
197
	}
198
199
	/**
200
	 * Returns the rounding exponent based on the given $quantity
201
	 * and the @see QuantityFormatter::OPT_APPLY_ROUNDING option.
202
	 *
203
	 * @param QuantityValue $quantity
204
	 *
205
	 * @return int|null
206
	 */
207
	private function getRoundingExponent( QuantityValue $quantity ) {
208
		if ( $this->options->getOption( self::OPT_APPLY_ROUNDING ) === true ) {
209
			return $this->options->getOption( self::OPT_SHOW_UNCERTAINTY_MARGIN )
210
				? null
211
				// round to the order of uncertainty
212
				: $quantity->getOrderOfUncertainty();
213
		} elseif ( $this->options->getOption( self::OPT_APPLY_ROUNDING ) === false ) {
214
			return null;
215
		} else {
216
			return (int)$this->options->getOption( self::OPT_APPLY_ROUNDING );
217
		}
218
	}
219
220
	/**
221
	 * @param DecimalValue $decimal
222
	 *
223
	 * @return string
224
	 */
225
	private function formatMinimalDecimal( DecimalValue $decimal ) {
226
		// TODO: This should be an option of DecimalFormatter.
227
		return preg_replace( '/(\.\d+?)0+$/', '$1',
228
			preg_replace( '/(?<=\d)\.0*$/', '', $this->decimalFormatter->format( $decimal ) )
229
		);
230
	}
231
232
	/**
233
	 * @param DecimalValue $margin
234
	 * @param int $roundingExponent
235
	 *
236
	 * @return string|null Text
237
	 */
238
	private function formatMargin( DecimalValue $margin, $roundingExponent ) {
239
		if ( $this->options->getOption( self::OPT_SHOW_UNCERTAINTY_MARGIN ) ) {
240
			// TODO: never round to 0! See bug #56892
241
			$roundedMargin = $this->decimalMath->roundToExponent( $margin, $roundingExponent );
242
243
			if ( !$roundedMargin->isZero() ) {
244
				return $this->decimalFormatter->format( $roundedMargin );
245
			}
246
		}
247
248
		return null;
249
	}
250
251
	/**
252
	 * @since 0.6
253
	 *
254
	 * @param string $unit URI
255
	 *
256
	 * @return string|null Text
257
	 */
258
	protected function formatUnit( $unit ) {
259
		if ( $this->vocabularyUriFormatter === null
260
			|| !$this->options->getOption( self::OPT_APPLY_UNIT )
261
			|| $unit === ''
262
			|| $unit === '1'
263
		) {
264
			return null;
265
		}
266
267
		return $this->vocabularyUriFormatter->format( $unit );
268
	}
269
270
}
271