AbstractMathAdapter   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.42%

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 4
dl 0
loc 257
ccs 61
cts 66
cp 0.9242
rs 9.2
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getSupportedRoundingStrategies() 0 9 1
A createNewInvalidNumberException() 0 4 2
A createNewUnknownErrorException() 0 4 1
A getLibraryResult() 0 21 4
getDefaultDelegates() 0 1 ?
A __construct() 0 15 4
A getNumberPrecision() 0 9 2
A getRoundingStrategy() 0 4 1
A getPrecision() 0 8 3
A getDelegates() 0 8 4
A isRealNumber() 0 4 3
B getOperationType() 0 24 5
B getDelegateResult() 0 29 4
1
<?php
2
3
namespace Tdn\PhpTypes\Math;
4
5
use Tdn\PhpTypes\Exception\InvalidNumberException;
6
use Tdn\PhpTypes\Math\Library\MathLibraryInterface;
7
use Tdn\PhpTypes\Type\StringType;
8
9
/**
10
 * Class AbstractMathAdapter.
11
 */
12
abstract class AbstractMathAdapter implements MathAdapterInterface
13
{
14
    /**
15
     * @var NumberValidatorInterface
16
     */
17
    private $validator;
18
19
    /**
20
     * @var array|MathLibraryInterface[]
21
     */
22
    private $delegates = [];
23
24
    /**
25
     * @var int
26
     */
27
    private $roundingStrategy;
28
29
    /**
30
     * @param NumberValidatorInterface|null $validator
31
     * @param MathLibraryInterface|null     $delegate
32
     * @param int                           $roundingStrategy
33
     *
34
     * @throws \OutOfBoundsException when a rounding strategy is passed as argument and not supported
35
     */
36 85
    public function __construct(
37
        NumberValidatorInterface $validator = null,
38
        MathLibraryInterface $delegate = null,
39
        int $roundingStrategy = PHP_ROUND_HALF_UP
40
    ) {
41 85
        if ($roundingStrategy !== null && !in_array($roundingStrategy, static::getSupportedRoundingStrategies())) {
42 1
            throw new \OutOfBoundsException(
43 1
                'Unsupported rounding strategy. Please refer to PHP\'s documentation on rounding.'
44
            );
45
        }
46
47 85
        $this->validator = $validator ?? new DefaultNumberValidator();
48 85
        $this->roundingStrategy = $roundingStrategy;
49 85
        $this->delegates = $delegate ? [$delegate] : $this->getDefaultDelegates();
50 85
    }
51
52
    /**
53
     * @param $number
54
     *
55
     * @return int
56
     */
57 35
    public static function getNumberPrecision($number): int
58
    {
59 35
        $string = StringType::valueOf($number);
60 35
        if ($string->contains('.')) {
61 24
            return $string->substr(($string->indexOf('.') + 1), $string->length())->count();
62
        }
63
64 28
        return 0;
65
    }
66
67
    /**
68
     * @return int
69
     */
70 85
    public function getRoundingStrategy(): int
71
    {
72 85
        return $this->roundingStrategy;
73
    }
74
75
    /**
76
     * Returns the precision of number.
77
     *
78
     * @param string $number
79
     *
80
     * @return int
81
     */
82 35
    public function getPrecision($number): int
83
    {
84 35
        if ($this->validator->isValid($number)) {
85 34
            return static::getNumberPrecision($number);
86
        }
87
88 1
        throw new InvalidNumberException(sprintf('Invalid number: %s', ($number ?: gettype($number))));
89
    }
90
91
    /**
92
     * @return MathLibraryInterface[]
93
     */
94
    abstract protected function getDefaultDelegates(): array;
95
96
    /**
97
     * Iterates through libraries to operate on.
98
     *
99
     * @param string $type
100
     *
101
     * @return \Generator|MathLibraryInterface[]
102
     */
103 48
    protected function getDelegates(string $type): \Generator
104
    {
105 48
        foreach ($this->delegates as $library) {
106 48
            if ($library->isEnabled() && $library->supportsOperationType($type)) {
107 48
                yield $library;
108
            }
109
        }
110
    }
111
112
    /**
113
     * @param string $type
114
     * @param string $leftOperand
115
     * @param string $rightOperand
116
     *
117
     * @return bool
118
     */
119 12
    protected function isRealNumber(string $type, string $leftOperand, string $rightOperand = '0')
120
    {
121 12
        return !($type !== self::TYPE_INT || $leftOperand < 0 || $rightOperand < 0);
122
    }
123
124
    /**
125
     * Ensures operands are valid and returns the operation type.
126
     *
127
     * @param string      $a
128
     * @param string|null $b
129
     *
130
     * @throws InvalidNumberException when an operand is not a valid number
131
     *
132
     * @return string
133
     */
134
    protected function getOperationType(string $a, string $b = null): string
135
    {
136 50
        $getType = function ($v, $previousType = null) {
137 49
            $previousType = $previousType ?? self::TYPE_INT;
138
139 49
            return (strpos($v, '.') !== false) ? self::TYPE_FLOAT : $previousType;
140 50
        };
141
142 50
        if (!$this->validator->isValid($a)) {
143 1
            throw $this->createNewInvalidNumberException($a);
144
        }
145
146 49
        $type = $getType($a);
147
148 49
        if ($b !== null) {
149 23
            if (!$this->validator->isValid($b)) {
150 1
                throw $this->createNewInvalidNumberException($b);
151
            }
152
153 22
            $type = $getType($b, $type);
154
        }
155
156 48
        return $type;
157
    }
158
159
    /**
160
     * Much like a "chain-of-responsibility" this method iterates through the available delegates, attempting to perform
161
     * the desired operation if it exists.
162
     * If the operation fails due to a library error, it will try the next library. If all libraries fail then
163
     * it will use the last exception thrown.
164
     *
165
     * @param string      $operation
166
     * @param string      $leftOperand
167
     * @param string|null $rightOperand
168
     * @param int|null    $precision
169
     * @param string|null $overrideType
170
     *
171
     * @return mixed
172
     */
173 44
    protected function getDelegateResult(
174
        string $operation,
175
        string $leftOperand,
176
        string $rightOperand = null,
177
        int $precision = null,
178
        string $overrideType = null
179
    ) {
180 44
        $type = $overrideType ?? $this->getOperationType($leftOperand, $rightOperand);
181 42
        $exception = null;
182
183 42
        foreach ($this->getDelegates($type) as $library) {
184
            // In case of future interface changes between delegates, let's see if the method is callable.
185
            // If not, we'll skip to the next library.
186 42
            if (!is_callable([$library, $operation])) {
187
                continue;
188
            }
189
190
            try {
191 42
                return $this->getLibraryResult($library, $operation, $leftOperand, $rightOperand, $precision);
192 21
            } catch (\Throwable $e) {
193
                // Save last exception and try next library.
194 21
                $exception = new \RuntimeException($e->getMessage(), $e->getCode(), $e);
195 21
                continue;
196
            }
197
        }
198
199
        //We'll use the last exception thrown, otherwise create one.
200
        throw $exception ?? $this->createNewUnknownErrorException();
201
    }
202
203
    /**
204
     * Supported rounding strategies.
205
     *
206
     * @return array<int>
207
     */
208 85
    protected static function getSupportedRoundingStrategies(): array
209
    {
210
        return [
211 85
            PHP_ROUND_HALF_UP,
212 85
            PHP_ROUND_HALF_DOWN,
213 85
            PHP_ROUND_HALF_EVEN,
214 85
            PHP_ROUND_HALF_ODD,
215
        ];
216
    }
217
218
    /**
219
     * @param $num
220
     *
221
     * @return InvalidNumberException
222
     */
223 2
    protected function createNewInvalidNumberException($num)
224
    {
225 2
        return new InvalidNumberException(sprintf('Invalid number: %s', ($num ?: gettype($num))));
226
    }
227
228
    /**
229
     * @return \RuntimeException
230
     */
231
    protected function createNewUnknownErrorException()
232
    {
233
        return new \RuntimeException('Unknown error.');
234
    }
235
236
    /**
237
     * This method tries to call the operation with the proper number of arguments based on whether they are null.
238
     *
239
     * @param MathLibraryInterface $library
240
     * @param string               $operation
241
     * @param string               $leftOperand
242
     * @param string|null          $rightOperand
243
     * @param int|null             $precision
244
     *
245
     * @return mixed
246
     */
247 42
    private function getLibraryResult(
248
        MathLibraryInterface $library,
249
        string $operation,
250
        string $leftOperand,
251
        string $rightOperand = null,
252
        int $precision = null
253
    ) {
254 42
        if ($precision !== null) {
255 27
            if ($rightOperand !== null) {
256 21
                return $library->$operation($leftOperand, $rightOperand, $precision);
257
            }
258
259 6
            return $library->$operation($leftOperand, $precision);
260
        }
261
262 15
        if ($rightOperand !== null) {
263 3
            return $library->$operation($leftOperand, $rightOperand);
264
        }
265
266 12
        return $library->$operation($leftOperand);
267
    }
268
}
269