Math   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 268
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 30
c 4
b 1
f 0
lcom 1
cbo 1
dl 0
loc 268
ccs 76
cts 76
cp 1
rs 10

15 Methods

Rating   Name   Duplication   Size   Complexity  
A truncate() 0 4 1
A bcRoundHalfUp() 0 4 1
A normalizePrecision() 0 4 2
A isNotDecimalString() 0 4 1
A isFirstDecimalAfterPrecisionTrailedByZeros() 0 7 1
A getHalfUpValue() 0 6 1
A roundNotTied() 0 8 2
B roundTied() 0 24 5
A getSign() 0 8 2
A getEvenOddDigit() 0 9 2
A getOddRoundedResult() 0 9 2
A getEvenRoundedResult() 0 9 2
A getFirstDecimalAfterPrecision() 0 7 1
A normalizeZero() 0 10 3
B bcround() 0 29 4
1
<?php namespace Keios\MoneyRight;
2
3
/**
4
 * This file is part of the arbitrary precision arithmetic-based
5
 * money value object Keios\MoneyRight package. Keios\MoneyRight
6
 * is heavily inspired by Mathias Verroes' Money library and was
7
 * designed to be a drop-in replacement (only some use statement
8
 * tweaking required) for mentioned library. Public APIs are
9
 * identical, functionality is extended with additional methods
10
 * and parameters.
11
 *
12
 *
13
 * Copyright (c) 2015 Łukasz Biały
14
 *
15
 * For the full copyright and license information, please view the
16
 * LICENSE file that was distributed with this source code.
17
 */
18
19
use Keios\MoneyRight\Exceptions\InvalidArgumentException;
20
21
/**
22
 * Class Math
23
 * Provides arbitrary precision rounding using BCMath extension
24
 *
25
 * @package Keios\MoneyRight
26
 */
27
class Math
28
{
29
    /**
30
     * @const int HALF - rounding point
31
     */
32
    const HALF = 5;
33
34
    /**
35
     * @const
36
     */
37
    const ROUND_HALF_UP = PHP_ROUND_HALF_UP;
38
39
    /**
40
     * @const
41
     */
42
    const ROUND_HALF_DOWN = PHP_ROUND_HALF_DOWN;
43
44
    /**
45
     * @const
46
     */
47
    const ROUND_HALF_EVEN = PHP_ROUND_HALF_EVEN;
48
49
    /**
50
     * @const
51
     */
52
    const ROUND_HALF_ODD = PHP_ROUND_HALF_ODD;
53
54
    /**
55
     * @param $number
56
     * @param $precision
57
     * @return bool
58
     */
59 21
    final private static function isFirstDecimalAfterPrecisionTrailedByZeros($number, $precision)
60
    {
61 21
        $secondPlaceAfterPrecision = strpos($number, '.') + $precision + 2;
62 21
        $remainingDecimals = substr($number, $secondPlaceAfterPrecision);
63
64 21
        return bccomp($remainingDecimals, '0', 64) === 1;
65
    }
66
67
    /**
68
     * @param $number
69
     * @param $precision
70
     * @return string
71
     */
72 183
    final private static function getHalfUpValue($number, $precision)
73
    {
74 183
        $sign = self::getSign($number);;
75
76 183
        return $sign . '0.' . str_repeat('0', $precision) . '5';
77
    }
78
79
    /**
80
     * @param $number
81
     * @param $precision
82
     * @return string
83
     */
84 183
    final private static function truncate($number, $precision)
85
    {
86 183
        return bcadd($number, '0', $precision);
87
    }
88
89
90
    /**
91
     * @param $firstDecimalAfterPrecision
92
     * @param $number
93
     * @param $precision
94
     * @return string
95
     */
96 12
    final private static function roundNotTied($firstDecimalAfterPrecision, $number, $precision)
97
    {
98 12
        if ($firstDecimalAfterPrecision > self::HALF) {
99 9
            return self::bcRoundHalfUp($number, $precision);
100
        } else {
101 12
            return self::truncate($number, $precision);
102
        }
103
    }
104
105
    /**
106
     * @param $number
107
     * @param $precision
108
     * @param $roundingMode
109
     * @return string
110
     * @throws InvalidArgumentException
111
     */
112 21
    final private static function roundTied($number, $precision, $roundingMode)
113
    {
114 21
        if (self::isFirstDecimalAfterPrecisionTrailedByZeros($number, $precision)) {
115
116 9
            $result = self::bcRoundHalfUp($number, $precision);
117
118 9
        } else {
119
            switch ($roundingMode) {
120 21
                case self::ROUND_HALF_DOWN:
121 9
                    $result = self::truncate($number, $precision);
122 9
                    break;
123 15
                case self::ROUND_HALF_EVEN:
124 9
                    $result = self::getEvenRoundedResult($number, $precision);
125 9
                    break;
126 9
                case self::ROUND_HALF_ODD:
127 6
                    $result = self::getOddRoundedResult($number, $precision);
128 6
                    break;
129 3
                default:
130 3
                    throw new InvalidArgumentException('Rounding mode should be Money::ROUND_HALF_DOWN | Money::ROUND_HALF_EVEN | Money::ROUND_HALF_ODD | Money::ROUND_HALF_UP');
131 3
            }
132
        }
133
134 18
        return $result;
135
    }
136
137
    /**
138
     * @param $number
139
     * @return string
140
     */
141 183
    final private static function getSign($number)
142
    {
143 183
        if (bccomp('0', $number, 64) == 1) {
144 36
            return '-';
145
        } else {
146 168
            return '';
147
        }
148
    }
149
150
    /**
151
     * @param $number
152
     * @param $precision
153
     * @return int
154
     */
155 12
    final private static function getEvenOddDigit($number, $precision)
156
    {
157 12
        list($integers, $decimals) = explode('.', $number);
158 12
        if ($precision === 0) {
159 3
            return (int)substr($integers, -1);
160
        } else {
161 9
            return (int)$decimals[$precision - 1];
162
        }
163
    }
164
165
    /**
166
     * @param $number
167
     * @param $precision
168
     * @return string
169
     */
170 6
    final private static function getOddRoundedResult($number, $precision)
171
    {
172 6
        if (self::getEvenOddDigit($number, $precision) % 2) { // odd
173 3
            return self::truncate($number, $precision);
174
        } else { // even
175 3
            return self::truncate(self::bcRoundHalfUp($number, $precision), $precision);
176
        }
177
178
    }
179
180
    /**
181
     * @param $number
182
     * @param $precision
183
     * @return string
184
     */
185 9
    final private static function getEvenRoundedResult($number, $precision)
186
    {
187 9
        if (self::getEvenOddDigit($number, $precision) % 2) { // odd
188 3
            return self::bcRoundHalfUp($number, $precision);
189
        } else { // even
190 6
            return self::truncate($number, $precision);
191
        }
192
193
    }
194
195
    /**
196
     * @param $number
197
     * @param $precision
198
     * @return int
199
     */
200 21
    final private static function getFirstDecimalAfterPrecision($number, $precision)
201
    {
202 21
        $decimals = explode('.', $number)[1];
203 21
        $firstDecimalAfterPrecision = (int)substr($decimals, $precision, 1);
204
205 21
        return $firstDecimalAfterPrecision;
206
    }
207
208
    /**
209
     * Round decimals from 5 up, less than 5 down
210
     *
211
     * @param $number
212
     * @param $precision
213
     *
214
     * @return string
215
     */
216 183
    final private static function bcRoundHalfUp($number, $precision)
217
    {
218 183
        return self::truncate(bcadd($number, self::getHalfUpValue($number, $precision), $precision + 1), $precision);
219
    }
220
221
    /**
222
     * @param $precision
223
     * @return int
224
     */
225 216
    final private static function normalizePrecision($precision)
226
    {
227 216
        return ($precision < 0) ? 0 : $precision;
228
    }
229
230
    /**
231
     * @param $number
232
     * @return bool
233
     */
234 216
    final private static function isNotDecimalString($number)
235
    {
236 216
        return strpos($number, '.') === false;
237
    }
238
239
    /**
240
     * @param $result
241
     * @param $precision
242
     * @return string
243
     */
244 18
    final private static function normalizeZero($result, $precision)
245
    {
246 18
        if ($result[0] === '-') {
247 9
            if (bccomp(substr($result, 1), '0', $precision) === 0) {
248 6
                return '0';
249
            }
250 9
        }
251
252 18
        return $result;
253
    }
254
255
    /**
256
     * BCRound implementation
257
     *
258
     * @param     $number
259
     * @param     $precision
260
     * @param int $roundingMode
261
     *
262
     * @return string
263
     * @throws \Keios\MoneyRight\Exceptions\InvalidArgumentException
264
     */
265 216
    final public static function bcround($number, $precision, $roundingMode = self::ROUND_HALF_UP)
266
    {
267 216
        $precision = self::normalizePrecision($precision);
268
269 216
        if (self::isNotDecimalString($number)) {
270 69
            return bcadd($number, '0', $precision);
271
        }
272
273 186
        if ($roundingMode === self::ROUND_HALF_UP) {
274 174
            return self::bcRoundHalfUp($number, $precision);
275
        }
276
277 21
        $firstDecimalAfterPrecision = self::getFirstDecimalAfterPrecision($number, $precision);
278
279 21
        if ($firstDecimalAfterPrecision === self::HALF) {
280 21
            $result = self::roundTied($number, $precision, $roundingMode);
281 18
        } else {
282 12
            $result = self::roundNotTied($firstDecimalAfterPrecision, $number, $precision);
283
        }
284
285
        /*
286
         * Arbitrary precision arithmetic allows for '-0.0' which is not equal to '0.0' if compared with bccomp.
287
         * We have no use for this behaviour, so negative numbers have to be checked if they are minus zero,
288
         * so we can convert them into unsigned zero and return that.
289
         */
290 18
        $result = self::normalizeZero($result, $precision);
291
292 18
        return $result;
293
    }
294
}
295