Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.20% |
65 / 69 |
|
62.50% |
5 / 8 |
CRAP | |
0.00% |
0 / 1 |
Regex | |
94.20% |
65 / 69 |
|
62.50% |
5 / 8 |
34.23 | |
0.00% |
0 / 1 |
__construct | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
normalizePattern | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
12 | |||
escape | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isMatch | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
matches | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
8 | |||
replace | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
replaceCallback | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
split | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace PeServer\Core; |
6 | |
7 | use PeServer\Core\Collection\Arr; |
8 | use PeServer\Core\Collection\OrderBy; |
9 | use PeServer\Core\Errors\ErrorHandler; |
10 | use PeServer\Core\Throws\ArgumentException; |
11 | use PeServer\Core\Throws\RegexDelimiterException; |
12 | use PeServer\Core\Throws\RegexException; |
13 | use PeServer\Core\Throws\RegexPatternException; |
14 | |
15 | /** |
16 | * 正規表現ラッパー。 |
17 | */ |
18 | class Regex |
19 | { |
20 | #region define |
21 | |
22 | public const UNLIMITED = -1; |
23 | private const DELIMITER_CLOSE_START_INDEX = 2; |
24 | |
25 | #endregion |
26 | |
27 | #region variable |
28 | |
29 | private static ?Encoding $firstDefaultEncoding = null; |
30 | private readonly Encoding $encoding; |
31 | |
32 | #endregion |
33 | |
34 | /** |
35 | * 生成。 |
36 | * |
37 | * @param Encoding|null $encoding UTF-8の場合 /pattern/u の u を追加する用。標準エンコーディング(mb_internal_encoding: 基本UTF-8)を想定。 |
38 | */ |
39 | public function __construct(?Encoding $encoding = null) |
40 | { |
41 | if ($encoding === null) { |
42 | if (self::$firstDefaultEncoding === null) { |
43 | self::$firstDefaultEncoding = Encoding::getDefaultEncoding(); |
44 | } |
45 | $this->encoding = self::$firstDefaultEncoding; |
46 | } else { |
47 | $this->encoding = $encoding; |
48 | } |
49 | } |
50 | |
51 | #region function |
52 | |
53 | private function normalizePattern(string $pattern): string |
54 | { |
55 | $byteLength = strlen($pattern); |
56 | if ($byteLength < 3) { |
57 | throw new RegexPatternException($pattern); |
58 | } |
59 | |
60 | $open = $pattern[0]; |
61 | $closeIndex = match ($open) { |
62 | '(' => strrpos($pattern, ')', self::DELIMITER_CLOSE_START_INDEX), |
63 | '{' => strrpos($pattern, '}', self::DELIMITER_CLOSE_START_INDEX), |
64 | '[' => strrpos($pattern, ']', self::DELIMITER_CLOSE_START_INDEX), |
65 | '<' => strrpos($pattern, '>', self::DELIMITER_CLOSE_START_INDEX), |
66 | default => strrpos($pattern, $open, self::DELIMITER_CLOSE_START_INDEX), |
67 | }; |
68 | if ($closeIndex === false || $closeIndex === 0) { |
69 | throw new RegexDelimiterException(); |
70 | } |
71 | |
72 | //UTF8対応 |
73 | if ($this->encoding->name === Encoding::ENCODE_UTF8) { |
74 | if ($closeIndex === ($byteLength - 1)) { |
75 | return $pattern . 'u'; |
76 | } |
77 | if (strpos($pattern, 'u', $closeIndex) === false) { |
78 | return $pattern . 'u'; |
79 | } |
80 | } |
81 | |
82 | return $pattern; |
83 | } |
84 | |
85 | /** |
86 | * 正規表現パターンをエスケープコードに変換。 |
87 | * |
88 | * `preg_quote` ラッパー。 |
89 | * |
90 | * @param string $s 正規表現パターン。 |
91 | * @param string|null $delimiter デリミタ(null: `/`)。 |
92 | * @return string |
93 | * @see https://www.php.net/manual/function.preg-quote.php |
94 | */ |
95 | public function escape(string $s, ?string $delimiter = null): string |
96 | { |
97 | return preg_quote($s, $delimiter); |
98 | } |
99 | |
100 | /** |
101 | * パターンにマッチするか。 |
102 | * |
103 | * @param string $input 対象文字列。 |
104 | * @param string $pattern 正規表現パターン。 |
105 | * @phpstan-param literal-string $pattern |
106 | * @return boolean マッチしたか。 |
107 | * @throws RegexException 正規表現処理失敗。 |
108 | */ |
109 | public function isMatch(string $input, string $pattern): bool |
110 | { |
111 | $result = ErrorHandler::trap(fn () => preg_match($this->normalizePattern($pattern), $input)); |
112 | if ($result->isFailureOrFalse()) { |
113 | throw new RegexException(preg_last_error_msg(), preg_last_error()); |
114 | } |
115 | |
116 | return (bool)$result->value; |
117 | } |
118 | |
119 | /** |
120 | * パターンマッチ。 |
121 | * |
122 | * @param string $input |
123 | * @param string $pattern |
124 | * @phpstan-param literal-string $pattern |
125 | * @return array<int|string,string> |
126 | * @phpstan-return array<array-key,string> |
127 | */ |
128 | public function matches(string $input, string $pattern): array |
129 | { |
130 | $matches = []; |
131 | $result = ErrorHandler::trap(function () use ($pattern, $input, &$matches) { |
132 | return preg_match_all($this->normalizePattern($pattern), $input, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE); |
133 | }); |
134 | if ($result->isFailureOrFalse()) { |
135 | throw new RegexException(preg_last_error_msg(), preg_last_error()); |
136 | } |
137 | if ($result->value === 0) { |
138 | return []; |
139 | } |
140 | |
141 | // 最初のやつは無かったことにする |
142 | array_shift($matches); |
143 | |
144 | $items = []; |
145 | foreach ($matches as $key => $match) { |
146 | if (is_int($key)) { |
147 | foreach ($match as $v) { |
148 | $items[$v[1]] = $v[0]; |
149 | } |
150 | } else { |
151 | $items[$key] = $match[0][0]; |
152 | } |
153 | } |
154 | $items = Arr::sortByKey($items, OrderBy::Ascending); |
155 | |
156 | $resultMatches = [ |
157 | 0 => $input, |
158 | ]; |
159 | |
160 | foreach ($items as $key => $item) { |
161 | if (is_int($key)) { |
162 | $resultMatches[] = $item; |
163 | } else { |
164 | $resultMatches[$key] = $item; |
165 | } |
166 | } |
167 | |
168 | return $resultMatches; |
169 | } |
170 | |
171 | /** |
172 | * 正規表現置き換え。 |
173 | * |
174 | * @param string $source 一致する対象を検索する文字列。 |
175 | * @param string $pattern 一致させる正規表現パターン。 |
176 | * @param string $replacement 置換文字列。 |
177 | * @param int $limit 各パターンによる 置換を行う最大回数。 |
178 | * @throws ArgumentException 引数がおかしい。 |
179 | * @throws RegexException 正規表現処理失敗。 |
180 | * @return string |
181 | * @see https://www.php.net/manual/function.preg-replace.php |
182 | */ |
183 | public function replace(string $source, string $pattern, string $replacement, int $limit = self::UNLIMITED): string |
184 | { |
185 | if (!$limit) { |
186 | throw new ArgumentException(); |
187 | } |
188 | |
189 | $result = ErrorHandler::trap(fn () => preg_replace($this->normalizePattern($pattern), $replacement, $source, $limit)); |
190 | if ($result->isFailureOrFailValue(null)) { |
191 | throw new RegexException(preg_last_error_msg(), preg_last_error()); |
192 | } |
193 | |
194 | return $result->value; |
195 | } |
196 | |
197 | |
198 | /** |
199 | * 正規表現置き換え。 |
200 | * |
201 | * @param string $source 一致する対象を検索する文字列。 |
202 | * @param string $pattern 一致させる正規表現パターン。 |
203 | * @param callable $replacement 置換処理。 |
204 | * @phpstan-param callable(array<array-key,string>):string $replacement |
205 | * @param int $limit 各パターンによる 置換を行う最大回数。 |
206 | * @throws ArgumentException 引数がおかしい。 |
207 | * @throws RegexException 正規表現処理失敗。 |
208 | * @return string |
209 | * @see https://www.php.net/manual/function.preg-replace-callback.php |
210 | */ |
211 | public function replaceCallback(string $source, string $pattern, callable $replacement, int $limit = self::UNLIMITED): string |
212 | { |
213 | if (!$limit) { |
214 | throw new ArgumentException(); |
215 | } |
216 | |
217 | $result = preg_replace_callback($this->normalizePattern($pattern), $replacement, $source, $limit); |
218 | if ($result === null) { |
219 | throw new RegexException(preg_last_error_msg(), preg_last_error()); |
220 | } |
221 | |
222 | return $result; |
223 | } |
224 | |
225 | /** |
226 | * 分割処理。 |
227 | * |
228 | * @param string $source 入力文字列。 |
229 | * @param string $pattern パターン。 |
230 | * @return string[] |
231 | */ |
232 | public function split(string $source, string $pattern): array |
233 | { |
234 | $result = preg_split($this->normalizePattern($pattern), $source); |
235 | if ($result === false) { |
236 | throw new RegexException(preg_last_error_msg(), preg_last_error()); |
237 | } |
238 | |
239 | return $result; |
240 | } |
241 | |
242 | #endregion |
243 | } |