Completed
Push — master ( 8ea799...e22a3b )
by
unknown
23s
created

QuantityFormatter::formatNumber()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 2 Features 1
Metric Value
c 3
b 2
f 1
dl 0
loc 5
rs 9.4285
cc 2
eloc 4
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
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 $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,
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 an UnboundedQuantityValue.' );
122
		}
123
124
		$formatted = $this->formatNumber( $value );
125
		$unit = $this->formatUnit( $value->getUnit() );
126
127
		if ( $unit !== null ) {
128
			$formatted = strtr(
129
				$this->quantityWithUnitFormat,
130
				array(
131
					'$1' => $formatted,
132
					'$2' => $unit
133
				)
134
			);
135
		}
136
137
		return $formatted;
138
	}
139
140
	/**
141
	 * @since 0.8.2
142
	 *
143
	 * @param UnboundedQuantityValue $quantity
144
	 *
145
	 * @return string Text
146
	 */
147
	protected function formatNumber( UnboundedQuantityValue $quantity ) {
148
		return $quantity instanceof QuantityValue
149
			? $this->formatQuantityValue( $quantity )
150
			: $this->formatUnboundedQuantityValue( $quantity );
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
	 * @param QuantityValue $quantity
171
	 *
172
	 * @return string Text
173
	 */
174
	private function formatQuantityValue( QuantityValue $quantity ) {
175
		$roundingExponent = $this->getRoundingExponent( $quantity );
176
177
		$amount = $quantity->getAmount();
178
179
		if ( $roundingExponent === null ) {
180
			$formatted = $this->formatMinimalDecimal( $amount );
181
			$margin = $quantity->getUncertaintyMargin();
182
			$margin = $margin->isZero() ? null : $this->formatMinimalDecimal( $margin );
183
		} else {
184
			$roundedAmount = $this->decimalMath->roundToExponent( $amount, $roundingExponent );
185
			$formatted = $this->decimalFormatter->format( $roundedAmount );
186
			$margin = $this->formatMargin( $quantity->getUncertaintyMargin(), $roundingExponent );
187
		}
188
189
		if ( $margin !== null ) {
190
			// TODO: use localizable pattern for constructing the output.
191
			$formatted .= '±' . $margin;
192
		}
193
194
		return $formatted;
195
	}
196
197
	/**
198
	 * Returns the rounding exponent based on the given $quantity
199
	 * and the @see QuantityFormatter::OPT_APPLY_ROUNDING option.
200
	 *
201
	 * @param QuantityValue $quantity
202
	 *
203
	 * @return int|null
204
	 */
205
	private function getRoundingExponent( QuantityValue $quantity ) {
206
		if ( $this->options->getOption( self::OPT_APPLY_ROUNDING ) === true ) {
207
			return $this->options->getOption( self::OPT_SHOW_UNCERTAINTY_MARGIN )
208
				? null
209
				// round to the order of uncertainty
210
				: $quantity->getOrderOfUncertainty();
211
		} elseif ( $this->options->getOption( self::OPT_APPLY_ROUNDING ) === false ) {
212
			return null;
213
		} else {
214
			return (int)$this->options->getOption( self::OPT_APPLY_ROUNDING );
215
		}
216
	}
217
218
	/**
219
	 * @param DecimalValue $decimal
220
	 *
221
	 * @return string
222
	 */
223
	private function formatMinimalDecimal( DecimalValue $decimal ) {
224
		// TODO: This should be an option of DecimalFormatter.
225
		return $this->decimalFormatter->format( $decimal->getTrimmed() );
226
	}
227
228
	/**
229
	 * @param DecimalValue $margin
230
	 * @param int $roundingExponent
231
	 *
232
	 * @return string|null Text
233
	 */
234
	private function formatMargin( DecimalValue $margin, $roundingExponent ) {
235
		if ( $this->options->getOption( self::OPT_SHOW_UNCERTAINTY_MARGIN ) ) {
236
			$roundedMargin = $this->decimalMath->roundToExponent( $margin, $roundingExponent );
237
238
			if ( !$roundedMargin->isZero() ) {
239
				return $this->decimalFormatter->format( $roundedMargin );
240
			}
241
		}
242
243
		return null;
244
	}
245
246
	/**
247
	 * @since 0.6
248
	 *
249
	 * @param string $unit URI
250
	 *
251
	 * @return string|null Text
252
	 */
253
	protected function formatUnit( $unit ) {
254
		if ( !$this->options->getOption( self::OPT_APPLY_UNIT )
255
			|| $unit === ''
256
			|| $unit === '1'
257
		) {
258
			return null;
259
		}
260
261
		return $this->vocabularyUriFormatter->format( $unit );
262
	}
263
264
}
265