Completed
Push — optional-bounds ( a6997b )
by Daniel
02:35
created

QuantityFormatter::formatQuantityValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 2
Metric Value
c 3
b 0
f 2
dl 0
loc 16
rs 9.4285
cc 2
eloc 10
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
 * @licence GNU GPL v2+
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->getQuantityWithUnitFormat(),
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 );
0 ignored issues
show
Bug introduced by
It seems like $quantity->getUncertaintyMargin() can be null; however, formatMargin() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
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->vocabularyUriFormatter === null
221
			|| !$this->options->getOption( self::OPT_APPLY_UNIT )
222
			|| $unit === ''
223
			|| $unit === '1'
224
		) {
225
			return null;
226
		}
227
228
		return $this->vocabularyUriFormatter->format( $unit );
229
	}
230
231
}
232