QuantityFormatter::formatQuantityValue()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 9
cts 9
cp 1
rs 9.568
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3
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-or-later
17
 * @author Daniel Kinzler
18
 * @author Thiemo Kreuz
19
 */
20
class QuantityFormatter implements ValueFormatter {
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
	public 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
	public 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
	public const OPT_APPLY_UNIT = 'applyUnit';
50
51
	/**
52
	 * @var FormatterOptions
53
	 */
54
	private $options;
55
56
	/**
57
	 * @var DecimalMath
58
	 */
59
	private $decimalMath;
60
61
	/**
62
	 * @var DecimalFormatter
63
	 */
64
	private $decimalFormatter;
65
66
	/**
67
	 * @var ValueFormatter
68
	 */
69
	private $vocabularyUriFormatter;
70
71
	/**
72
	 * @var string
73
	 */
74
	private $quantityWithUnitFormat;
75
76
	/**
77
	 * @since 0.6
78
	 *
79
	 * @param FormatterOptions|null $options
80
	 * @param DecimalFormatter|null $decimalFormatter
81
	 * @param ValueFormatter $vocabularyUriFormatter
82 61
	 * @param string|null $quantityWithUnitFormat Format string with two placeholders, $1 for the
83
	 * number and $2 for the unit. Warning, this must be under the control of the application, not
84
	 * under the control of the user, because it allows HTML injections in subclasses that return
85
	 * HTML.
86
	 */
87
	public function __construct(
88 61
		?FormatterOptions $options,
89
		?DecimalFormatter $decimalFormatter,
90 61
		ValueFormatter $vocabularyUriFormatter,
91 61
		$quantityWithUnitFormat = null
92 61
	) {
93
		$this->options = $options ?: new FormatterOptions();
94 61
95 61
		$this->options->defaultOption( ValueFormatter::OPT_LANG, 'en' );
96 61
		$this->options->defaultOption( self::OPT_SHOW_UNCERTAINTY_MARGIN, true );
97
		$this->options->defaultOption( self::OPT_APPLY_ROUNDING, true );
98
		$this->options->defaultOption( self::OPT_APPLY_UNIT, true );
99 61
100 61
		$this->decimalFormatter = $decimalFormatter ?: new DecimalFormatter( $this->options );
101
		$this->vocabularyUriFormatter = $vocabularyUriFormatter;
102
		$this->quantityWithUnitFormat = $quantityWithUnitFormat ?: '$1 $2';
103
104
		// plain composition should be sufficient
105
		$this->decimalMath = new DecimalMath();
106
	}
107
108
	/**
109
	 * @since 0.6
110
	 *
111
	 * @return string
112
	 */
113
	final protected function getQuantityWithUnitFormat() {
114
		return $this->quantityWithUnitFormat;
115
	}
116
117
	/**
118
	 * @see ValueFormatter::format
119 61
	 *
120 61
	 * @param UnboundedQuantityValue|QuantityValue $value
121
	 *
122
	 * @throws InvalidArgumentException
123
	 * @return string Text
124 61
	 */
125 61
	public function format( $value ) {
126
		if ( !( $value instanceof UnboundedQuantityValue ) ) {
127 61
			throw new InvalidArgumentException( 'Data value type mismatch. Expected an UnboundedQuantityValue.' );
128 14
		}
129 14
130
		$formatted = $this->formatNumber( $value );
131 14
		$unit = $this->formatUnit( $value->getUnit() );
132 14
133
		if ( $unit !== null ) {
134
			$formatted = strtr(
135
				$this->quantityWithUnitFormat,
136
				[
137 61
					'$1' => $formatted,
138
					'$2' => $unit
139
				]
140
			);
141
		}
142
143
		return $formatted;
144
	}
145
146
	/**
147 61
	 * @since 0.8.2
148 61
	 *
149 47
	 * @param UnboundedQuantityValue $quantity
150 61
	 *
151
	 * @return string Text
152
	 */
153
	protected function formatNumber( UnboundedQuantityValue $quantity ) {
154
		return $quantity instanceof QuantityValue
155
			? $this->formatQuantityValue( $quantity )
156
			: $this->formatUnboundedQuantityValue( $quantity );
157
	}
158 14
159 14
	/**
160 14
	 * @param UnboundedQuantityValue $quantity
161
	 *
162 14
	 * @return string
163 8
	 */
164
	protected function formatUnboundedQuantityValue( UnboundedQuantityValue $quantity ) {
165
		$amount = $quantity->getAmount();
166 14
		$roundingExponent = $this->options->getOption( self::OPT_APPLY_ROUNDING );
167
168
		if ( !is_bool( $roundingExponent ) ) {
169
			$amount = $this->decimalMath->roundToExponent( $amount, (int)$roundingExponent );
170
		}
171
172
		return $this->decimalFormatter->format( $amount );
173
	}
174 47
175 47
	/**
176
	 * @param QuantityValue $quantity
177 47
	 *
178
	 * @return string Text
179 47
	 */
180 26
	private function formatQuantityValue( QuantityValue $quantity ) {
181 26
		$roundingExponent = $this->getRoundingExponent( $quantity );
182 26
183
		$amount = $quantity->getAmount();
184 21
185 21
		if ( $roundingExponent === null ) {
186 21
			$formatted = $this->formatMinimalDecimal( $amount );
187
			$margin = $quantity->getUncertaintyMargin();
188
			$margin = $this->formatMinimalDecimal( $margin );
189 47
		} else {
190
			$roundedAmount = $this->decimalMath->roundToExponent( $amount, $roundingExponent );
191 26
			$formatted = $this->decimalFormatter->format( $roundedAmount );
192
			$margin = $this->formatMargin( $quantity->getUncertaintyMargin(), $roundingExponent );
193
		}
194 47
195
		if ( $margin !== null ) {
196
			// TODO: use localizable pattern for constructing the output.
197
			$formatted .= '±' . $margin;
198
		}
199
200
		return $formatted;
201
	}
202
203
	/**
204
	 * Returns the rounding exponent based on the given $quantity
205 47
	 * and the @see QuantityFormatter::OPT_APPLY_ROUNDING option.
206 47
	 *
207 41
	 * @param QuantityValue $quantity
208 24
	 *
209
	 * @return int|null
210 41
	 */
211 6
	private function getRoundingExponent( QuantityValue $quantity ) {
212 2
		if ( $this->options->getOption( self::OPT_APPLY_ROUNDING ) === true ) {
213
			return $this->options->getOption( self::OPT_SHOW_UNCERTAINTY_MARGIN )
214 4
				? null
215
				// round to the order of uncertainty
216
				: $quantity->getOrderOfUncertainty();
217
		} elseif ( $this->options->getOption( self::OPT_APPLY_ROUNDING ) === false ) {
218
			return null;
219
		} else {
220
			return (int)$this->options->getOption( self::OPT_APPLY_ROUNDING );
221
		}
222
	}
223 26
224
	/**
225 26
	 * @param DecimalValue $decimal
226
	 *
227
	 * @return string
228
	 */
229
	private function formatMinimalDecimal( DecimalValue $decimal ) {
230
		// TODO: This should be an option of DecimalFormatter.
231
		return $this->decimalFormatter->format( $decimal->getTrimmed() );
232
	}
233
234 21
	/**
235 21
	 * @param DecimalValue $margin
236
	 * @param int $roundingExponent
237
	 *
238
	 * @return string|null Text
239
	 */
240
	private function formatMargin( DecimalValue $margin, $roundingExponent ) {
241
		if ( $this->options->getOption( self::OPT_SHOW_UNCERTAINTY_MARGIN ) ) {
242
			$roundedMargin = $this->decimalMath->roundToExponent( $margin, $roundingExponent );
243 21
244
			if ( !$roundedMargin->isZero() ) {
245
				return $this->decimalFormatter->format( $roundedMargin );
246
			}
247
		}
248
249
		return null;
250
	}
251
252
	/**
253 61
	 * @since 0.6
254 61
	 *
255 60
	 * @param string $unit URI
256 61
	 *
257
	 * @return string|null Text
258 47
	 */
259
	protected function formatUnit( $unit ) {
260
		if ( !$this->options->getOption( self::OPT_APPLY_UNIT )
261 14
			|| $unit === ''
262
			|| $unit === '1'
263
		) {
264
			return null;
265
		}
266
267
		return $this->vocabularyUriFormatter->format( $unit );
268
	}
269
270
}
271