HTMLPurifier_UnitConverter::mul()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 3
1
<?php
2
3
/**
4
 * Class for converting between different unit-lengths as specified by
5
 * CSS.
6
 */
7
class HTMLPurifier_UnitConverter
8
{
9
10
    const ENGLISH = 1;
11
    const METRIC = 2;
12
    const DIGITAL = 3;
13
14
    /**
15
     * Units information array. Units are grouped into measuring systems
16
     * (English, Metric), and are assigned an integer representing
17
     * the conversion factor between that unit and the smallest unit in
18
     * the system. Numeric indexes are actually magical constants that
19
     * encode conversion data from one system to the next, with a O(n^2)
20
     * constraint on memory (this is generally not a problem, since
21
     * the number of measuring systems is small.)
22
     */
23
    protected static $units = array(
24
        self::ENGLISH => array(
25
            'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
26
            'pt' => 4,
27
            'pc' => 48,
28
            'in' => 288,
29
            self::METRIC => array('pt', '0.352777778', 'mm'),
30
        ),
31
        self::METRIC => array(
32
            'mm' => 1,
33
            'cm' => 10,
34
            self::ENGLISH => array('mm', '2.83464567', 'pt'),
35
        ),
36
    );
37
38
    /**
39
     * Minimum bcmath precision for output.
40
     * @type int
41
     */
42
    protected $outputPrecision;
43
44
    /**
45
     * Bcmath precision for internal calculations.
46
     * @type int
47
     */
48
    protected $internalPrecision;
49
50
    /**
51
     * Whether or not BCMath is available.
52
     * @type bool
53
     */
54
    private $bcmath;
55
56
    public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false)
57
    {
58
        $this->outputPrecision = $output_precision;
59
        $this->internalPrecision = $internal_precision;
60
        $this->bcmath = !$force_no_bcmath && function_exists('bcmul');
61
    }
62
63
    /**
64
     * Converts a length object of one unit into another unit.
65
     * @param HTMLPurifier_Length $length
66
     *      Instance of HTMLPurifier_Length to convert. You must validate()
67
     *      it before passing it here!
68
     * @param string $to_unit
69
     *      Unit to convert to.
70
     * @return HTMLPurifier_Length|bool
71
     * @note
72
     *      About precision: This conversion function pays very special
73
     *      attention to the incoming precision of values and attempts
74
     *      to maintain a number of significant figure. Results are
75
     *      fairly accurate up to nine digits. Some caveats:
76
     *          - If a number is zero-padded as a result of this significant
77
     *            figure tracking, the zeroes will be eliminated.
78
     *          - If a number contains less than four sigfigs ($outputPrecision)
79
     *            and this causes some decimals to be excluded, those
80
     *            decimals will be added on.
81
     */
82
    public function convert($length, $to_unit)
83
    {
84
        if (!$length->isValid()) {
85
            return false;
86
        }
87
88
        $n = $length->getN();
89
        $unit = $length->getUnit();
90
91
        if ($n === '0' || $unit === false) {
92
            return new HTMLPurifier_Length('0', false);
93
        }
94
95
        $state = $dest_state = false;
96
        foreach (self::$units as $k => $x) {
97
            if (isset($x[$unit])) {
98
                $state = $k;
99
            }
100
            if (isset($x[$to_unit])) {
101
                $dest_state = $k;
102
            }
103
        }
104
        if (!$state || !$dest_state) {
105
            return false;
106
        }
107
108
        // Some calculations about the initial precision of the number;
109
        // this will be useful when we need to do final rounding.
110
        $sigfigs = $this->getSigFigs($n);
111
        if ($sigfigs < $this->outputPrecision) {
112
            $sigfigs = $this->outputPrecision;
113
        }
114
115
        // BCMath's internal precision deals only with decimals. Use
116
        // our default if the initial number has no decimals, or increase
117
        // it by how ever many decimals, thus, the number of guard digits
118
        // will always be greater than or equal to internalPrecision.
119
        $log = (int)floor(log(abs($n), 10));
0 ignored issues
show
Bug introduced by
$n of type string is incompatible with the type double|integer expected by parameter $num of abs(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

119
        $log = (int)floor(log(abs(/** @scrutinizer ignore-type */ $n), 10));
Loading history...
120
        $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
121
122
        for ($i = 0; $i < 2; $i++) {
123
124
            // Determine what unit IN THIS SYSTEM we need to convert to
125
            if ($dest_state === $state) {
126
                // Simple conversion
127
                $dest_unit = $to_unit;
128
            } else {
129
                // Convert to the smallest unit, pending a system shift
130
                $dest_unit = self::$units[$state][$dest_state][0];
131
            }
132
133
            // Do the conversion if necessary
134
            if ($dest_unit !== $unit) {
135
                $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
136
                $n = $this->mul($n, $factor, $cp);
137
                $unit = $dest_unit;
138
            }
139
140
            // Output was zero, so bail out early. Shouldn't ever happen.
141
            if ($n === '') {
142
                $n = '0';
143
                $unit = $to_unit;
144
                break;
145
            }
146
147
            // It was a simple conversion, so bail out
148
            if ($dest_state === $state) {
149
                break;
150
            }
151
152
            if ($i !== 0) {
153
                // Conversion failed! Apparently, the system we forwarded
154
                // to didn't have this unit. This should never happen!
155
                return false;
156
            }
157
158
            // Pre-condition: $i == 0
159
160
            // Perform conversion to next system of units
161
            $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);
162
            $unit = self::$units[$state][$dest_state][2];
163
            $state = $dest_state;
164
165
            // One more loop around to convert the unit in the new system.
166
167
        }
168
169
        // Post-condition: $unit == $to_unit
170
        if ($unit !== $to_unit) {
171
            return false;
172
        }
173
174
        // Useful for debugging:
175
        //echo "<pre>n";
176
        //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n</pre>\n";
177
178
        $n = $this->round($n, $sigfigs);
0 ignored issues
show
Bug introduced by
$n of type string is incompatible with the type double expected by parameter $n of HTMLPurifier_UnitConverter::round(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

178
        $n = $this->round(/** @scrutinizer ignore-type */ $n, $sigfigs);
Loading history...
179
        if (strpos($n, '.') !== false) {
180
            $n = rtrim($n, '0');
181
        }
182
        $n = rtrim($n, '.');
183
184
        return new HTMLPurifier_Length($n, $unit);
185
    }
186
187
    /**
188
     * Returns the number of significant figures in a string number.
189
     * @param string $n Decimal number
190
     * @return int number of sigfigs
191
     */
192
    public function getSigFigs($n)
193
    {
194
        $n = ltrim($n, '0+-');
195
        $dp = strpos($n, '.'); // decimal position
196
        if ($dp === false) {
197
            $sigfigs = strlen(rtrim($n, '0'));
198
        } else {
199
            $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character
200
            if ($dp !== 0) {
201
                $sigfigs--;
202
            }
203
        }
204
        return $sigfigs;
205
    }
206
207
    /**
208
     * Adds two numbers, using arbitrary precision when available.
209
     * @param string $s1
210
     * @param string $s2
211
     * @param int $scale
212
     * @return string
213
     */
214
    private function add($s1, $s2, $scale)
0 ignored issues
show
Unused Code introduced by
The method add() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
215
    {
216
        if ($this->bcmath) {
217
            return bcadd($s1, $s2, $scale);
218
        } else {
219
            return $this->scale((float)$s1 + (float)$s2, $scale);
220
        }
221
    }
222
223
    /**
224
     * Multiples two numbers, using arbitrary precision when available.
225
     * @param string $s1
226
     * @param string $s2
227
     * @param int $scale
228
     * @return string
229
     */
230
    private function mul($s1, $s2, $scale)
231
    {
232
        if ($this->bcmath) {
233
            return bcmul($s1, $s2, $scale);
234
        } else {
235
            return $this->scale((float)$s1 * (float)$s2, $scale);
236
        }
237
    }
238
239
    /**
240
     * Divides two numbers, using arbitrary precision when available.
241
     * @param string $s1
242
     * @param string $s2
243
     * @param int $scale
244
     * @return string
245
     */
246
    private function div($s1, $s2, $scale)
247
    {
248
        if ($this->bcmath) {
249
            return bcdiv($s1, $s2, $scale);
250
        } else {
251
            return $this->scale((float)$s1 / (float)$s2, $scale);
252
        }
253
    }
254
255
    /**
256
     * Rounds a number according to the number of sigfigs it should have,
257
     * using arbitrary precision when available.
258
     * @param float $n
259
     * @param int $sigfigs
260
     * @return string
261
     */
262
    private function round($n, $sigfigs)
263
    {
264
        $new_log = (int)floor(log(abs($n), 10)); // Number of digits left of decimal - 1
265
        $rp = $sigfigs - $new_log - 1; // Number of decimal places needed
266
        $neg = $n < 0 ? '-' : ''; // Negative sign
267
        if ($this->bcmath) {
268
            if ($rp >= 0) {
269
                $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1);
270
                $n = bcdiv($n, '1', $rp);
271
            } else {
272
                // This algorithm partially depends on the standardized
273
                // form of numbers that comes out of bcmath.
274
                $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0);
275
                $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1);
276
            }
277
            return $n;
278
        } else {
279
            return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);
280
        }
281
    }
282
283
    /**
284
     * Scales a float to $scale digits right of decimal point, like BCMath.
285
     * @param float $r
286
     * @param int $scale
287
     * @return string
288
     */
289
    private function scale($r, $scale)
290
    {
291
        if ($scale < 0) {
292
            // The f sprintf type doesn't support negative numbers, so we
293
            // need to cludge things manually. First get the string.
294
            $r = sprintf('%.0f', (float)$r);
295
            // Due to floating point precision loss, $r will more than likely
296
            // look something like 4652999999999.9234. We grab one more digit
297
            // than we need to precise from $r and then use that to round
298
            // appropriately.
299
            $precise = (string)round(substr($r, 0, strlen($r) + $scale), -1);
0 ignored issues
show
Bug introduced by
substr($r, 0, strlen($r) + $scale) of type string is incompatible with the type double|integer expected by parameter $num of round(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

299
            $precise = (string)round(/** @scrutinizer ignore-type */ substr($r, 0, strlen($r) + $scale), -1);
Loading history...
300
            // Now we return it, truncating the zero that was rounded off.
301
            return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);
302
        }
303
        return sprintf('%.' . $scale . 'f', (float)$r);
304
    }
305
}
306
307
// vim: et sw=4 sts=4
308