Passed
Push — master ( ae0a03...686af4 )
by Magnar Ovedal
03:17
created

UpperCase::getNoncompliantCount()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 1
dl 0
loc 13
ccs 7
cts 7
cp 1
crap 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Stadly\PasswordPolice\Rule;
6
7
use InvalidArgumentException;
8
use Stadly\PasswordPolice\Password;
9
use Stadly\PasswordPolice\Policy;
10
11
final class UpperCase implements RuleInterface
12
{
13
    /**
14
     * @var int Minimum number of upper case letters.
15
     */
16
    private $min;
17
18
    /**
19
     * @var int|null Maximum number of upper case letters.
20
     */
21
    private $max;
22
23
    /**
24
     * @param int $min Minimum number of upper case letters.
25
     * @param int|null $max Maximum number of upper case letters.
26
     */
27 7
    public function __construct(int $min = 1, ?int $max = null)
28
    {
29 7
        if ($min < 0) {
30 1
            throw new InvalidArgumentException('Min cannot be negative.');
31
        }
32 6
        if ($max !== null && $max < $min) {
33 1
            throw new InvalidArgumentException('Max cannot be smaller than min.');
34
        }
35 5
        if ($min === 0 && $max === null) {
36 1
            throw new InvalidArgumentException('Min cannot be zero when max is unconstrained.');
37
        }
38
39 4
        $this->min = $min;
40 4
        $this->max = $max;
41 4
    }
42
43
    /**
44
     * @return int Minimum number of upper case letters.
45
     */
46 1
    public function getMin(): int
47
    {
48 1
        return $this->min;
49
    }
50
51
    /**
52
     * @return int|null Maximum number of upper case letters.
53
     */
54 1
    public function getMax(): ?int
55
    {
56 1
        return $this->max;
57
    }
58
59
    /**
60
     * Check whether a password is in compliance with the rule.
61
     *
62
     * @param Password|string $password Password to check.
63
     * @return bool Whether the password is in compliance with the rule.
64
     */
65 6
    public function test($password): bool
66
    {
67 6
        $count = $this->getNoncompliantCount((string)$password);
68
69 6
        return $count === null;
70
    }
71
72
    /**
73
     * Enforce that a password is in compliance with the rule.
74
     *
75
     * @param Password|string $password Password that must adhere to the rule.
76
     * @throws RuleException If the password does not adhrere to the rule.
77
     */
78 2
    public function enforce($password): void
79
    {
80 2
        $count = $this->getNoncompliantCount((string)$password);
81
82 2
        if ($count !== null) {
83 1
            throw new RuleException($this, $this->getMessage());
84
        }
85 1
    }
86
87
    /**
88
     * @param string $password Password to count characters in.
89
     * @return int Number of upper case characters if not in compliance with the rule.
90
     */
91 8
    private function getNoncompliantCount(string $password): ?int
92
    {
93 8
        $count = $this->getCount($password);
94
95 8
        if ($count < $this->min) {
96 3
            return $count;
97
        }
98
99 5
        if (null !== $this->max && $this->max < $count) {
100 1
            return $count;
101
        }
102
103 4
        return null;
104
    }
105
106
    /**
107
     * @param string $password Password to count characters in.
108
     * @return int Number of upper case characters.
109
     */
110 8
    private function getCount(string $password): int
111
    {
112 8
        $lowerCase = mb_strtolower($password);
113
114 8
        $passwordCharacters = $this->splitString($password);
115 8
        $lowerCaseCharacters = $this->splitString($lowerCase);
116 8
        assert(count($passwordCharacters) === count($lowerCaseCharacters));
117
118 8
        $count = 0;
119 8
        for ($i = count($passwordCharacters)-1; $i >= 0; --$i) {
120 8
            if ($passwordCharacters[$i] !== $lowerCaseCharacters[$i]) {
121 5
                ++$count;
122
            }
123
        }
124
125 8
        return $count;
126
    }
127
128
    /**
129
     * @param string $string String to split into individual characters.
130
     * @return string[] Array of characters.
131
     */
132 8
    private function splitString(string $string): array
133
    {
134 8
        $characters = preg_split('{}u', $string, -1, PREG_SPLIT_NO_EMPTY);
135 8
        assert($characters !== false);
136
137 8
        return $characters;
138
    }
139
140
    /**
141
     * {@inheritDoc}
142
     */
143 5
    public function getMessage(): string
144
    {
145 5
        $translator = Policy::getTranslator();
146
147 5
        if ($this->max === null) {
148 1
            return $translator->trans(
149
                'There must be at least one upper case character.|'.
150 1
                'There must be at least %count% upper case characters.',
151 1
                ['%count%' => $this->min]
152
            );
153
        }
154
155 4
        if ($this->max === 0) {
156 1
            return $translator->trans(
157 1
                'There must be no upper case characters.'
158
            );
159
        }
160
161 3
        if ($this->min === 0) {
162 1
            return $translator->trans(
163
                'There must be at most one upper case character.|'.
164 1
                'There must be at most %count% upper case characters.',
165 1
                ['%count%' => $this->max]
166
            );
167
        }
168
169 2
        if ($this->min === $this->max) {
170 1
            return $translator->trans(
171
                'There must be exactly one upper case character.|'.
172 1
                'There must be exactly %count% upper case characters.',
173 1
                ['%count%' => $this->min]
174
            );
175
        }
176
177 1
        return $translator->trans(
178 1
            'There must be between %min% and %max% upper case characters.',
179 1
            ['%min%' => $this->min, '%max%' => $this->max]
180
        );
181
    }
182
}
183