Completed
Pull Request — master (#64)
by no
03:14 queued 48s
created

QuantityFormatter::formatUnit()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 10
rs 9.2
cc 4
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 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
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 $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,
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
		$roundedAmount = $this->decimalMath->roundToExponent( $amount, $roundingExponent );
162
		$formatted = $this->decimalFormatter->format( $roundedAmount );
163
164
		$margin = $this->formatMargin( $quantity->getUncertaintyMargin(), $roundingExponent );
165
		if ( $margin !== null ) {
166
			// TODO: use localizable pattern for constructing the output.
167
			$formatted .= '±' . $margin;
168
		}
169
170
		return $formatted;
171
	}
172
173
	/**
174
	 * Returns the rounding exponent based on the given $quantity
175
	 * and the @see QuantityFormatter::OPT_APPLY_ROUNDING option.
176
	 *
177
	 * @param QuantityValue $quantity
178
	 *
179
	 * @return int
180
	 */
181
	private function getRoundingExponent( QuantityValue $quantity ) {
182
		if ( $this->options->getOption( self::OPT_APPLY_ROUNDING ) === true ) {
183
			// round to the order of uncertainty
184
			return $quantity->getOrderOfUncertainty();
185
		} elseif ( $this->options->getOption( self::OPT_APPLY_ROUNDING ) === false ) {
186
			// to keep all digits, use the negative length of the fractional part
187
			return -strlen( $quantity->getAmount()->getFractionalPart() );
188
		} else {
189
			return (int)$this->options->getOption( self::OPT_APPLY_ROUNDING );
190
		}
191
	}
192
193
	/**
194
	 * @param DecimalValue $margin
195
	 * @param int $roundingExponent
196
	 *
197
	 * @return string|null Text
198
	 */
199
	private function formatMargin( DecimalValue $margin, $roundingExponent ) {
200
		if ( $this->options->getOption( self::OPT_SHOW_UNCERTAINTY_MARGIN ) ) {
201
			// TODO: never round to 0! See bug #56892
202
			$roundedMargin = $this->decimalMath->roundToExponent( $margin, $roundingExponent );
203
204
			if ( !$roundedMargin->isZero() ) {
205
				return $this->decimalFormatter->format( $roundedMargin );
206
			}
207
		}
208
209
		return null;
210
	}
211
212
	/**
213
	 * @since 0.6
214
	 *
215
	 * @param string $unit URI
216
	 *
217
	 * @return string|null Text
218
	 */
219
	protected function formatUnit( $unit ) {
220
		if ( !$this->options->getOption( self::OPT_APPLY_UNIT )
221
			|| $unit === ''
222
			|| $unit === '1'
223
		) {
224
			return null;
225
		}
226
227
		return $this->vocabularyUriFormatter->format( $unit );
228
	}
229
230
}
231