Issues (14)

src/DigitCalculator.php (2 issues)

1
<?php
2
3
namespace Brazanation\Documents;
4
5
/**
6
 * Class DigitCalculator is inspired in DigitoPara class from Java built by Caleum
7
 *
8
 * A fluent interface to calculate digits, used for any Boletos and document numbers.
9
 *
10
 * For example, the digit from 0000039104766 with the multipliers starting from 2 until 7 and using module11,
11
 * follow:
12
 *
13
 * <pre>
14
 *    0  0  0  0  0  3  9  1  0  4  7  6  6 (numeric section)
15
 *    2  7  6  5  4  3  2  7  6  5  4  3  2 (multipliers, from right to left and in cycle)
16
 *    ----------------------------------------- multiplication digit by digit
17
 *     0  0  0  0  0  9 18  7  0 20 28 18 12 -- sum = 112
18
 * </pre>
19
 *
20
 * Gets module from this sum, so, does calculate the additional from module and, if number is 0, 10 or 11,
21
 * the digit result will be 1.
22
 *
23
 * <pre>
24
 *        sum = 112
25
 *        sum % 11 = 2
26
 *        11 - (sum % 11) = 9
27
 * </pre>
28
 *
29
 * @package Brazanation\Documents
30
 *
31
 * @see     https://github.com/caelum/caelum-stella/blob/master/stella-core/src/main/java/br/com/caelum/stella/DigitoPara.java
32
 */
33
class DigitCalculator
34
{
35
    const MODULE_10 = 10;
36
37
    const MODULE_11 = 11;
38
39
    /**
40
     * A list for digits.
41
     *
42
     * @var \ArrayObject
43
     */
44
    protected $number;
45
46
    /**
47
     * A list of integer multipliers.
48
     *
49
     * @var \ArrayObject
50
     */
51
    protected $multipliers;
52
53
    /**
54
     *
55
     * @var bool
56
     */
57
    protected $additional = false;
58
59
    /**
60
     * @var int
61
     */
62
    protected $module = DigitCalculator::MODULE_11;
63
64
    /**
65
     * @var bool
66
     */
67
    protected $singleSum;
68
69
    /**
70
     * @var \ArrayObject
71
     */
72
    private $replacements;
73
74
    /**
75
     * @var int
76
     */
77
    private $sumMultiplier;
78
79
    /**
80
     * Creates object to be filled with fluent interface and store a numeric section into
81
     * a list of digits. It is required because the numeric section could be so bigger than a integer number supports.
82
     *
83
     * @param string $number Base numeric section to be calculate your digit.
84
     */
85 1059
    public function __construct(string $number)
86
    {
87 1059
        $this->number = new \ArrayObject(str_split(strrev($number)));
0 ignored issues
show
It seems like str_split(strrev($number)) can also be of type true; however, parameter $array of ArrayObject::__construct() does only seem to accept array|object, maybe add an additional type check? ( Ignorable by Annotation )

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

87
        $this->number = new \ArrayObject(/** @scrutinizer ignore-type */ str_split(strrev($number)));
Loading history...
88 1059
        $this->multipliers = new \ArrayObject();
89 1059
        $this->replacements = new \ArrayObject();
90
91 1059
        $this->withMultipliersInterval(2, 9);
92 1059
        $this->withModule(static::MODULE_11);
93 1059
        $this->multiplySumBy(1);
94
    }
95
96
    /**
97
     * Sequential multipliers (or coefficient) and ascending order, this method allow
98
     * to create a list of multipliers.
99
     *
100
     * It will be used in cycle, when the base number is larger than multipliers sequence.
101
     * By default, multipliers are started with 2-9.
102
     *
103
     * You can enter another value and this default will be overwritten.
104
     *
105
     * @param int $start First number of sequential interval of multipliers
106
     * @param int $end   Last number of sequential interval of multipliers
107
     *
108
     * @return DigitCalculator
109
     */
110 1059
    public function withMultipliersInterval(int $start, int $end) : DigitCalculator
111
    {
112 1059
        $multipliers = [];
113 1059
        for ($i = $start; $i <= $end; ++$i) {
114 1059
            array_push($multipliers, $i);
115
        }
116
117 1059
        return $this->withMultipliers($multipliers);
118
    }
119
120
    /**
121
     * There are some documents in which the multipliers do not use all the numbers in a range or
122
     * change your order.
123
     *
124
     * In such cases, the multipliers list can be passed through array of integers.
125
     *
126
     * @param int[] $multipliers A list of integers sequence, such as: [9, 8, 7, 6, 5, 4, 3, 2, 1].
127
     *
128
     * @return DigitCalculator
129
     */
130 1059
    public function withMultipliers(array $multipliers) : DigitCalculator
131
    {
132 1059
        $multipliers = array_map(function ($multiplier) {
133 1059
            if (!assert(is_int($multiplier))) {
134
                throw new \InvalidArgumentException("The multiplier({$multiplier}) must be integer");
135
            }
136
137 1059
            return $multiplier;
138
        }, $multipliers);
139 1059
        $this->multipliers = new \ArrayObject($multipliers);
140
141 1059
        return $this;
142
    }
143
144
    /**
145
     * It is common digit generators need additional module instead of module itself.
146
     *
147
     * So to call this method enables a flag that is used in module method to decide
148
     * if the returned result is pure module or its complementary.
149
     *
150
     * @return DigitCalculator
151
     */
152 982
    public function useComplementaryInsteadOfModule() : DigitCalculator
153
    {
154 982
        $this->additional = true;
155
156 982
        return $this;
157
    }
158
159
    /**
160
     * There are some documents with specific rules for calculated digits.
161
     *
162
     * Some cases is possible to find X as digit checker.
163
     *
164
     * @param string $replaceTo A string to replace a digit.
165
     * @param int[]  $integers  A list of numbers to be replaced by $replaceTo
166
     *
167
     * @return DigitCalculator
168
     */
169 1008
    public function replaceWhen(string $replaceTo, ...$integers) : DigitCalculator
170
    {
171 1008
        foreach ($integers as $integer) {
172 1008
            $this->replacements->offsetSet($integer, $replaceTo);
173
        }
174
175 1008
        return $this;
176
    }
177
178
    /**
179
     * Full whereby the rest will be taken and also its complementary.
180
     *
181
     * The default value is DigitCalculator::MODULE_11.
182
     *
183
     * @param int $module A integer to define module (DigitCalculator::MODULE_11 or DigitCalculator::MODULE_10)
184
     *
185
     * @return DigitCalculator
186
     */
187 1059
    public function withModule(int $module) : DigitCalculator
188
    {
189 1059
        $this->module = $module;
190
191 1059
        return $this;
192
    }
193
194
    /**
195
     * Indicates whether to calculate the module, the sum of the multiplication results
196
     * should be considered digit by digit.
197
     *
198
     * Eg: 2 * 9 = 18, sum = 9 (1 + 8) instead of 18
199
     *
200
     * @return DigitCalculator
201
     */
202
    public function singleSum() : DigitCalculator
203
    {
204
        $this->singleSum = true;
205
206
        return $this;
207
    }
208
209
    /**
210
     * Calculates the check digit from given numeric section.
211
     *
212
     * @return string Returns a single calculated digit.
213
     */
214 1059
    public function calculate() : string
215
    {
216 1059
        $sum = 0;
217 1059
        $position = 0;
218 1059
        foreach ($this->number as $digit) {
219 1059
            $multiplier = $this->multipliers->offsetGet($position);
220 1059
            $total = $digit * $multiplier;
221 1059
            $sum += $this->calculateSingleSum($total);
222 1059
            $position = $this->nextMultiplier($position);
223
        }
224
225 1059
        $sum = $this->calculateSumMultiplier($sum);
226
227 1059
        $result = $sum % $this->module;
228
229 1059
        $result = $this->calculateAdditionalDigit($result);
230
231 1059
        return $this->replaceDigit($result);
232
    }
233
234
    /**
235
     * Replaces the digit when mapped to be replaced by other digit.
236
     *
237
     * @param string $digit A digit to be replaced.
238
     *
239
     * @return string Returns digit replaced if it has been mapped, otherwise returns given digit.
240
     */
241 1059
    private function replaceDigit(string $digit) : string
242
    {
243 1059
        if ($this->replacements->offsetExists($digit)) {
244 230
            return $this->replacements->offsetGet($digit);
245
        }
246
247 904
        return $digit;
248
    }
249
250
    /**
251
     * Calculates additional digit when is additional is defined.
252
     *
253
     * @param string $digit A digit to be subtract from module.
254
     *
255
     * @return int Returns calculated digit.
256
     */
257 1059
    private function calculateAdditionalDigit(string $digit) : int
258
    {
259 1059
        if ($this->additional) {
260 982
            $digit = $this->module - $digit;
261
        }
262
263 1059
        return $digit;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $digit could return the type string which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
264
    }
265
266
    /**
267
     * Calculates single sum.
268
     *
269
     * @param int $total A total to be calculated.
270
     *
271
     * @return int Returns a calculated total.
272
     */
273 1059
    private function calculateSingleSum(int $total) : int
274
    {
275 1059
        if ($this->singleSum) {
276
            return (int) (($total / 10) + ($total % 10));
277
        }
278
279 1059
        return $total;
280
    }
281
282
    /**
283
     * Gets the next multiplier.
284
     *
285
     * @param int $position Current position.
286
     *
287
     * @return int Returns next position or zero (0) when it is greater than number of defined multipliers.
288
     */
289 1059
    private function nextMultiplier(int $position) : int
290
    {
291 1059
        ++$position;
292 1059
        if ($position == $this->multipliers->count()) {
293 1051
            $position = 0;
294
        }
295
296 1059
        return $position;
297
    }
298
299
    /**
300
     * Adds a digit into number collection.
301
     *
302
     * @param string $digit Digit to be prepended into number collection.
303
     *
304
     * @return DigitCalculator
305
     */
306 145
    public function addDigit(string $digit) : DigitCalculator
307
    {
308 145
        $numbers = $this->number->getArrayCopy();
309 145
        array_unshift($numbers, $digit);
310 145
        $this->number = new \ArrayObject($numbers);
311
312 145
        return $this;
313
    }
314
315
    /**
316
     * Defines the multiplier factor after calculate the sum of digits.
317
     *
318
     * @param int $multiplier A integer to multiply the sum result.
319
     *
320
     * @return DigitCalculator
321
     */
322 1059
    public function multiplySumBy(int $multiplier) : DigitCalculator
323
    {
324 1059
        $this->sumMultiplier = $multiplier;
325
326 1059
        return $this;
327
    }
328
329
    /**
330
     * Multiplies the sum result with defined multiplier factor.
331
     *
332
     * @param int $sum The result of calculation from digits.
333
     *
334
     * @return int
335
     */
336 1059
    private function calculateSumMultiplier(int $sum) : int
337
    {
338 1059
        return $this->sumMultiplier * $sum;
339
    }
340
}
341