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:: |