Completed
Push — master ( 80013f...49a5fb )
by Daniel
24s
created

QuantityFormatter::formatMinimalDecimal()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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