PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.4
WooCommerce v10.9.4
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / vendor / opis / string / src / UnicodeString.php
woocommerce / vendor / opis / string / src Last commit date
Exception 10 months ago UnicodeString.php 7 months ago
UnicodeString.php
1703 lines
1 <?php
2 /* ===========================================================================
3 * Copyright 2018-2021 Zindex Software
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 * ============================================================================ */
17
18 namespace Opis\String;
19
20 use RuntimeException;
21 use OutOfBoundsException;
22 use Countable, ArrayAccess;
23 use JsonSerializable;
24 use Opis\String\Exception\{
25 UnicodeException,
26 InvalidStringException,
27 InvalidCodePointException
28 };
29
30 class UnicodeString implements Countable, ArrayAccess, JsonSerializable
31 {
32 const KEEP_CASE = 0;
33
34 const LOWER_CASE = 1;
35
36 const UPPER_CASE = 2;
37
38 const FOLD_CASE = 3;
39
40 const ASCII_CONV = 4;
41
42 /**
43 * @var int[]
44 */
45 private array $codes;
46
47 /**
48 * @var string[]|null
49 */
50 private ?array $chars = null;
51 private int $length;
52 private ?string $str = null;
53 private ?array $cache = null;
54
55 /**
56 * @var int[][]
57 */
58 private static array $maps = [];
59
60 /**
61 * @param int[] $codes
62 */
63 private function __construct(array $codes = [])
64 {
65 $this->codes = $codes;
66 $this->length = count($codes);
67 }
68
69 /**
70 * @return int[]
71 */
72 public function codePoints(): array
73 {
74 return $this->codes;
75 }
76
77 /**
78 * @return string[]
79 */
80 public function chars(): array
81 {
82 if ($this->chars === null) {
83 $this->chars = self::getCharsFromCodePoints($this->codes);
84 }
85 return $this->chars;
86 }
87
88 /**
89 * @return int
90 */
91 public function length(): int
92 {
93 return $this->length;
94 }
95
96 /**
97 * @return bool
98 */
99 public function isEmpty(): bool
100 {
101 return $this->length === 0;
102 }
103
104 /**
105 * @param string|self|int[]|string[] $text
106 * @param bool $ignoreCase
107 * @return bool
108 */
109 public function equals($text, bool $ignoreCase = false): bool
110 {
111 return $this->compareTo($text, $ignoreCase) === 0;
112 }
113
114 /**
115 * @param string|self|int[]|string[] $text
116 * @param bool $ignoreCase
117 * @return int
118 */
119 public function compareTo($text, bool $ignoreCase = false): int
120 {
121 $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE;
122
123 $text = self::resolveCodePoints($text, $mode);
124
125 $length = count($text);
126
127 if ($length !== $this->length) {
128 return $this->length <=> $length;
129 }
130
131 return $this->getMappedCodes($mode) <=> $text;
132 }
133
134 /**
135 * @param string|self|int[]|string[] $text
136 * @param bool $ignoreCase
137 * @return bool
138 */
139 public function contains($text, bool $ignoreCase = false): bool
140 {
141 return $this->indexOf($text, 0, $ignoreCase) !== -1;
142 }
143
144 /**
145 * @param string|self|int[]|string[] $text
146 * @param bool $ignoreCase
147 * @return bool
148 */
149 public function startsWith($text, bool $ignoreCase = false): bool
150 {
151 $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE;
152
153 $text = self::resolveCodePoints($text, $mode);
154
155 $len = count($text);
156
157 if ($len === 0 || $len > $this->length) {
158 return false;
159 }
160
161 return array_slice($this->getMappedCodes($mode), 0, $len) === $text;
162 }
163
164 /**
165 * @param string|self|int[]|string[] $text
166 * @param bool $ignoreCase
167 * @return bool
168 */
169 public function endsWith($text, bool $ignoreCase = false): bool
170 {
171 $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE;
172
173 $text = self::resolveCodePoints($text, $mode);
174
175 if (empty($text)) {
176 return false;
177 }
178
179 $codes = $this->getMappedCodes($mode);
180
181 $offset = $this->length - count($text);
182
183 if ($offset < 0) {
184 return false;
185 }
186
187 return array_slice($codes, $offset) === $text;
188 }
189
190 /**
191 * @param string|self|int[]|string[] $text
192 * @param int $offset
193 * @param bool $ignoreCase
194 * @return int
195 */
196 public function indexOf($text, int $offset = 0, bool $ignoreCase = false): int
197 {
198 if ($offset < 0) {
199 $offset += $this->length;
200 }
201 if ($offset < 0 || $offset >= $this->length) {
202 return -1;
203 }
204
205 $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE;
206
207 $text = self::resolveCodePoints($text, $mode);
208
209 $len = count($text);
210
211 if ($len === 0 || $offset + $len > $this->length) {
212 return -1;
213 }
214
215 return $this->doIndexOf($this->getMappedCodes($mode), $text, $offset);
216 }
217
218 /**
219 * @param string|self|int[]|string[] $text
220 * @param int $offset
221 * @param bool $ignoreCase
222 * @return int
223 */
224 public function lastIndexOf($text, int $offset = 0, bool $ignoreCase = false): int
225 {
226 if ($offset < 0) {
227 $start = $this->length + $offset;
228 if ($start < 0) {
229 return -1;
230 }
231 $last = 0;
232 } else {
233 if ($offset >= $this->length) {
234 return -1;
235 }
236 $start = $this->length - 1;
237 $last = $offset;
238 }
239
240 $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE;
241
242 $text = self::resolveCodePoints($text, $mode);
243
244 $len = count($text);
245
246 if ($len === 0) {
247 return -1;
248 }
249
250 if ($offset < 0) {
251 if ($len > $this->length) {
252 return -1;
253 }
254 $start = min($start, $this->length - $len);
255 } elseif ($offset + $len > $this->length) {
256 return -1;
257 }
258
259 $codes = $this->getMappedCodes($mode);
260
261 for ($i = $start; $i >= $last; $i--) {
262 $match = true;
263
264 for ($j = 0; $j < $len; $j++) {
265 if ($codes[$i + $j] !== $text[$j]) {
266 $match = false;
267 break;
268 }
269 }
270
271 if ($match) {
272 return $i;
273 }
274 }
275
276 return -1;
277 }
278
279 /**
280 * @param string|self|int[]|string[] $text
281 * @param bool $ignoreCase
282 * @param bool $allowPrefixOnly If true the result can contain only the prefix
283 * @return $this
284 */
285 public function ensurePrefix($text, bool $ignoreCase = false, bool $allowPrefixOnly = true): self
286 {
287 $text = self::resolveCodePoints($text);
288
289 $len = count($text);
290
291 if ($len === 0) {
292 return clone $this;
293 }
294
295 if ($this->length === 0) {
296 return new static($text);
297 }
298
299 if ($ignoreCase) {
300 $prefix = self::getMappedCodePoints($text, self::FOLD_CASE);
301 } else {
302 $prefix = &$text;
303 }
304
305 if ($this->length === $len) {
306 $part = $this->getMappedCodes($ignoreCase ? self::FOLD_CASE : self::KEEP_CASE);
307 if ($allowPrefixOnly && $part === $prefix) {
308 return clone $this;
309 }
310 // Remove last element to avoid double check
311 array_pop($part);
312 } elseif ($this->length < $len) {
313 $part = $this->getMappedCodes($ignoreCase ? self::FOLD_CASE : self::KEEP_CASE);
314 // Checks if this can be a suffix
315 if ($allowPrefixOnly && (array_slice($prefix, 0, $this->length) === $part)) {
316 $text = array_slice($text, $this->length);
317 return new static(array_merge($this->codes, $text));
318 }
319 } else {
320 $part = array_slice($this->codes, 0, $len);
321 if ($ignoreCase) {
322 $part = self::getMappedCodePoints($part, self::FOLD_CASE);
323 }
324 if ($part === $prefix) {
325 return clone $this;
326 }
327 // Remove last element to avoid double check
328 array_pop($part);
329 }
330
331 $copy = $len;
332
333 $part_len = count($part);
334
335 while ($part_len) {
336 if ($part === array_slice($prefix, -$part_len)) {
337 $copy = $len - $part_len;
338 break;
339 }
340 array_pop($part);
341 $part_len--;
342 }
343
344 if ($copy === 0) {
345 return clone $this;
346 }
347
348 if ($copy < $len) {
349 $text = array_slice($text, 0, $copy);
350 }
351
352 return new static(array_merge($text, $this->codes));
353 }
354
355 /**
356 * @param string|self|int[]|string[] $text
357 * @param bool $ignoreCase
358 * @param bool $allowSuffixOnly If true the result can contain only the suffix
359 * @return static
360 */
361 public function ensureSuffix($text, bool $ignoreCase = false, bool $allowSuffixOnly = true): self
362 {
363 $text = self::resolveCodePoints($text);
364
365 $len = count($text);
366
367 if ($len === 0) {
368 return clone $this;
369 }
370
371 if ($this->length === 0) {
372 return new static($text);
373 }
374
375 if ($ignoreCase) {
376 $suffix = self::getMappedCodePoints($text, self::FOLD_CASE);
377 } else {
378 $suffix = &$text;
379 }
380
381 if ($this->length === $len) {
382 $part = $this->getMappedCodes($ignoreCase ? self::FOLD_CASE : self::KEEP_CASE);
383 if ($allowSuffixOnly && $part === $suffix) {
384 return clone $this;
385 }
386 // Remove first element to avoid double check
387 array_shift($part);
388 } elseif ($this->length < $len) {
389 $part = $this->getMappedCodes($ignoreCase ? self::FOLD_CASE : self::KEEP_CASE);
390 // Checks if this can be a prefix
391 if ($allowSuffixOnly && (array_slice($suffix, -$this->length) === $part)) {
392 $text = array_slice($text, 0, $len - $this->length);
393 return new static(array_merge($text, $this->codes));
394 }
395 } else {
396 $part = array_slice($this->codes, -$len);
397 if ($ignoreCase) {
398 $part = self::getMappedCodePoints($part, self::FOLD_CASE);
399 }
400 if ($part === $suffix) {
401 return clone $this;
402 }
403 // Remove first element to avoid double check
404 array_shift($part);
405 }
406
407 $skip = 0;
408
409 $part_len = count($part);
410
411 while ($part_len) {
412 if ($part === array_slice($suffix, 0, $part_len)) {
413 $skip = $part_len;
414 break;
415 }
416 array_shift($part);
417 $part_len--;
418 }
419
420 if ($skip === $len) {
421 return clone $this;
422 }
423
424 if ($skip) {
425 array_splice($text, 0, $skip);
426 }
427
428 return new static(array_merge($this->codes, $text));
429 }
430
431 /**
432 * @param string|self|int[]|string[] $text
433 * @param int $mode
434 * @return static
435 */
436 public function append($text, int $mode = self::KEEP_CASE): self
437 {
438 return new static(array_merge($this->codes, self::resolveCodePoints($text, $mode)));
439 }
440
441 /**
442 * @param string|self|int[]|string[] $text
443 * @param int $mode
444 * @return static
445 */
446 public function prepend($text, int $mode = self::KEEP_CASE): self
447 {
448 return new static(array_merge(self::resolveCodePoints($text, $mode), $this->codes));
449 }
450
451 /**
452 * @param string|self|int[]|string[] $text
453 * @param int $offset
454 * @param int $mode
455 * @return static
456 */
457 public function insert($text, int $offset, int $mode = self::KEEP_CASE): self
458 {
459 $codes = $this->codes;
460
461 array_splice($codes, $offset, 0, self::resolveCodePoints($text, $mode));
462
463 return new static($codes);
464 }
465
466 /**
467 * @param int $offset
468 * @param int|null $length
469 * @return static
470 */
471 public function remove(int $offset, ?int $length = null): self
472 {
473 $codes = $this->codes;
474
475 if ($length === null) {
476 array_splice($codes, $offset);
477 } else {
478 array_splice($codes, $offset, $length);
479 }
480
481 return new static($codes);
482 }
483
484 /**
485 * @param string|self|int[]|string[] $mask
486 * @return static
487 */
488 public function trim($mask = " \t\n\r\0\x0B"): self
489 {
490 return $this->doTrim($mask, true, true);
491 }
492
493 /**
494 * @param string|self|int[]|string[] $mask
495 * @return static
496 */
497 public function trimLeft($mask = " \t\n\r\0\x0B"): self
498 {
499 return $this->doTrim($mask, true, false);
500 }
501
502 /**
503 * @param string|self|int[]|string[] $mask
504 * @return static
505 */
506 public function trimRight($mask = " \t\n\r\0\x0B"): self
507 {
508 return $this->doTrim($mask, false, true);
509 }
510
511 /**
512 * @return static
513 */
514 public function reverse(): self
515 {
516 return new static(array_reverse($this->codes));
517 }
518
519 /**
520 * @param int $times
521 * @return static
522 */
523 public function repeat(int $times = 1): self
524 {
525 if ($times <= 1) {
526 return clone $this;
527 }
528
529 $codes = [];
530
531 while ($times--) {
532 $codes = array_merge($codes, $this->codes);
533 }
534
535 return new static($codes);
536 }
537
538 /**
539 * @param string|self|int[]|string[] $subject
540 * @param string|self|int[]|string[] $replace
541 * @param int $offset
542 * @param bool $ignoreCase
543 * @return static
544 */
545 public function replace($subject, $replace, int $offset = 0, bool $ignoreCase = false): self
546 {
547 if ($offset < 0) {
548 $offset += $this->length;
549 }
550 if ($offset < 0 || $offset >= $this->length) {
551 return clone $this;
552 }
553
554 $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE;
555
556 $subject = self::resolveCodePoints($subject, $mode);
557
558 $len = count($subject);
559
560 if ($len === 0 || $offset + $len > $this->length) {
561 return clone $this;
562 }
563
564 $offset = $this->doIndexOf($this->getMappedCodes($mode), $subject, $offset);
565
566 if ($offset === -1) {
567 return clone $this;
568 }
569
570 $codes = $this->codes;
571
572 array_splice($codes, $offset, count($subject), self::resolveCodePoints($replace));
573
574 return new static($codes);
575 }
576
577 /**
578 * @param string|self|int[]|string[] $subject
579 * @param string|self|int[]|string[] $replace
580 * @param bool $ignoreCase
581 * @param int $offset
582 * @return static
583 */
584 public function replaceAll($subject, $replace, int $offset = 0, bool $ignoreCase = false): self
585 {
586 if ($offset < 0) {
587 $offset += $this->length;
588 }
589 if ($offset < 0 || $offset >= $this->length) {
590 return clone $this;
591 }
592
593 $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE;
594
595 $subject = self::resolveCodePoints($subject, $mode);
596
597 $len = count($subject);
598
599 if ($len === 0 || $offset + $len > $this->length) {
600 return clone $this;
601 }
602
603 $replace = self::resolveCodePoints($replace);
604
605 $codes = $this->getMappedCodes($mode);
606
607 $copy = $this->codes;
608
609 $fix = count($replace) - $len;
610
611 $t = 0;
612
613 while (($pos = $this->doIndexOf($codes, $subject, $offset)) >= 0) {
614 array_splice($copy, $pos + $t * $fix, $len, $replace);
615 $offset = $pos + $len;
616 $t++;
617 }
618
619 return new static($copy);
620 }
621
622 /**
623 * @param string|self|int[]|string[] $delimiter
624 * @param bool $ignoreCase
625 * @return array
626 */
627 public function split($delimiter = '', bool $ignoreCase = false): array
628 {
629 $mode = $ignoreCase ? self::FOLD_CASE : self::KEEP_CASE;
630 $delimiter = self::resolveCodePoints($delimiter, $mode);
631 $len = count($delimiter);
632
633 $ret = [];
634
635 if ($len === 0) {
636 foreach ($this->codes as $code) {
637 $ret[] = new static([$code]);
638 }
639 } else {
640 $codes = $this->getMappedCodes($mode);
641
642 $offset = 0;
643
644 while (($pos = $this->doIndexOf($codes, $delimiter, $offset)) >= 0) {
645 $ret[] = new static(array_slice($this->codes, $offset, $pos - $offset));
646 $offset = $pos + $len;
647 }
648
649 $ret[] = new static(array_slice($this->codes, $offset));
650 }
651
652 return $ret;
653 }
654
655 /**
656 * @param int $start
657 * @param int|null $length
658 * @return static
659 */
660 public function substring(int $start, ?int $length = null): self
661 {
662 return new static(array_slice($this->codes, $start, $length));
663 }
664
665 /**
666 * @param int $size If negative then pad left otherwise pad right
667 * @param self|string|int $char A char or a code point
668 * @return static
669 */
670 public function pad(int $size, $char = 0x20): self
671 {
672 return new static(array_pad($this->codes, $size, self::resolveFirstCodePoint($char, 0x20)));
673 }
674
675 /**
676 * @param int $size
677 * @param self|string|int $char
678 * @return static
679 */
680 public function padLeft(int $size, $char = 0x20): self
681 {
682 if ($size > 0) {
683 $size = -$size;
684 }
685
686 return $this->pad($size, $char);
687 }
688
689 /**
690 * @param int $size
691 * @param self|string|int $char
692 * @return static
693 */
694 public function padRight(int $size, $char = 0x20): self
695 {
696 if ($size < 0) {
697 $size = -$size;
698 }
699
700 return $this->pad($size, $char);
701 }
702
703 /**
704 * @return bool
705 */
706 public function isLowerCase(): bool
707 {
708 return $this->isCase(self::LOWER_CASE);
709 }
710
711 /**
712 * @return bool
713 */
714 public function isUpperCase(): bool
715 {
716 return $this->isCase(self::UPPER_CASE);
717 }
718
719 /**
720 * @return bool
721 */
722 public function isAscii(): bool
723 {
724 $key = 'i' . self::ASCII_CONV;
725
726 if (!isset($this->cache[$key])) {
727 $ok = true;
728
729 foreach ($this->codes as $code) {
730 if ($code >= 0x80) {
731 $ok = false;
732 break;
733 }
734 }
735
736 $this->cache[$key] = $ok;
737 }
738
739 return $this->cache[$key];
740 }
741
742 /**
743 * Convert all chars to lower case (where possible)
744 * @return static
745 */
746 public function toLower(): self
747 {
748 if ($this->cache['i' . self::LOWER_CASE] ?? false) {
749 return clone $this;
750 }
751 return new static($this->getMappedCodes(self::LOWER_CASE));
752 }
753
754 /**
755 * Convert all chars to upper case (where possible)
756 * @return static
757 */
758 public function toUpper(): self
759 {
760 if ($this->cache['i' . self::UPPER_CASE] ?? false) {
761 return clone $this;
762 }
763 return new static($this->getMappedCodes(self::UPPER_CASE));
764 }
765
766 /**
767 * Converts all chars to their ASCII equivalent (if any)
768 * @return static
769 */
770 public function toAscii(): self
771 {
772 if ($this->cache['i' . self::ASCII_CONV] ?? false) {
773 return clone $this;
774 }
775 return new static($this->getMappedCodes(self::ASCII_CONV));
776 }
777
778 /**
779 * @param int $index
780 * @return string
781 */
782 public function charAt(int $index): string
783 {
784 // Allow negative index
785 if ($index < 0 && $index + $this->length >= 0) {
786 $index += $this->length;
787 }
788
789 if ($index < 0 || $index >= $this->length) {
790 return '';
791 }
792
793 return $this->chars()[$index];
794 }
795
796 /**
797 * @param int $index
798 * @return int
799 */
800 public function codePointAt(int $index): int
801 {
802 // Allow negative index
803 if ($index < 0 && $index + $this->length >= 0) {
804 $index += $this->length;
805 }
806
807 if ($index < 0 || $index >= $this->length) {
808 return -1;
809 }
810
811 return $this->codes[$index];
812 }
813
814 /**
815 * @param int $offset
816 * @return int
817 */
818 public function __invoke(int $offset): int
819 {
820 if ($offset < 0) {
821 if ($offset + $this->length < 0) {
822 throw new OutOfBoundsException("Undefined offset: {$offset}");
823 }
824 $offset += $this->length;
825 } elseif ($offset >= $this->length) {
826 throw new OutOfBoundsException("Undefined offset: {$offset}");
827 }
828
829 return $this->codes[$offset];
830 }
831
832 /**
833 * @inheritDoc
834 */
835 public function offsetExists($offset): bool
836 {
837 // Allow negative index
838 if ($offset < 0) {
839 $offset += $this->length;
840 }
841
842 return isset($this->codes[$offset]);
843 }
844
845 /**
846 * @inheritDoc
847 */
848 public function offsetGet($offset): string
849 {
850 if ($offset < 0) {
851 if ($offset + $this->length < 0) {
852 throw new OutOfBoundsException("Undefined offset: {$offset}");
853 }
854 $offset += $this->length;
855 } elseif ($offset >= $this->length) {
856 throw new OutOfBoundsException("Undefined offset: {$offset}");
857 }
858
859 return $this->chars()[$offset];
860 }
861
862 /**
863 * @inheritDoc
864 */
865 #[\ReturnTypeWillChange]
866 public function offsetSet($offset, $value)
867 {
868 // Allow negative index
869 if ($offset < 0) {
870 $offset += $this->length;
871 }
872
873 if (!isset($this->codes[$offset])) {
874 return;
875 }
876
877
878 $value = self::resolveFirstCodePoint($value);
879 if ($value === -1) {
880 return;
881 }
882
883 if ($value === $this->codes[$offset]) {
884 // Same value, nothing to do
885 return;
886 }
887
888 $this->codes[$offset] = $value;
889
890 // Clear cache
891 $this->str = null;
892 $this->cache = null;
893 if ($this->chars) {
894 $this->chars[$offset] = self::getCharFromCodePoint($value);
895 }
896 }
897
898 /**
899 * @inheritDoc
900 */
901 #[\ReturnTypeWillChange]
902 public function offsetUnset($offset)
903 {
904 throw new RuntimeException("Invalid operation");
905 }
906
907 /**
908 * @inheritDoc
909 */
910 public function count(): int
911 {
912 return $this->length;
913 }
914
915 /**
916 * @return string
917 */
918 public function __toString(): string
919 {
920 if ($this->str === null) {
921 $this->str = self::getStringFromCodePoints($this->codes);
922 }
923
924 return $this->str;
925 }
926
927 /**
928 * @inheritDoc
929 */
930 public function jsonSerialize(): string
931 {
932 return $this->__toString();
933 }
934
935 public function __serialize(): array
936 {
937 return [
938 'value' => $this->__toString(),
939 ];
940 }
941
942 public function __unserialize(array $data): void
943 {
944 $this->str = $data['value'];
945 $this->codes = self::getCodePointsFromString($this->str);
946 $this->length = count($this->codes);
947 }
948
949 /**
950 * Creates an unicode string instance from raw string
951 * @param string $string
952 * @param string|null $encoding Defaults to UTF-8
953 * @param int $mode
954 * @return static
955 * @throws InvalidStringException
956 */
957 public static function from(string $string, ?string $encoding = null, int $mode = self::KEEP_CASE): self
958 {
959 if ($encoding !== null && strcasecmp($encoding, 'UTF-8') !== 0) {
960 if (false === $string = @iconv($encoding, 'UTF-8', $string)) {
961 throw new UnicodeException("Could not convert string from '$encoding' encoding to UTF-8 encoding");
962 }
963 }
964
965 $instance = new static(self::getCodePointsFromString($string, $mode));
966 if ($mode === self::KEEP_CASE) {
967 $instance->str = $string;
968 }
969 return $instance;
970 }
971
972 /**
973 * Creates an unicode string instance from code points
974 * @param int[] $codes
975 * @param int $mode
976 * @return static
977 * @throws InvalidCodePointException
978 */
979 public static function fromCodePoints(array $codes, int $mode = self::KEEP_CASE): self
980 {
981 $map = self::getMapByMode($mode);
982
983 foreach ($codes as &$code) {
984 if (!is_int($codes) || !self::isValidCodePoint($code)) {
985 throw new InvalidCodePointException($code);
986 } else {
987 $code = $map[$code] ?? $code;
988 }
989 }
990
991 return new static(array_values($codes));
992 }
993
994 /**
995 * Converts the code point to corresponding char
996 * @param int $code
997 * @return string The char or an empty string if code point is invalid
998 */
999 public static function getCharFromCodePoint(int $code): string
1000 {
1001 if ($code < 0) {
1002 return '';
1003 }
1004
1005 if ($code < 0x80) {
1006 return chr($code);
1007 }
1008
1009 if ($code < 0x800) {
1010 return chr(($code >> 6) + 0xC0) . chr(($code & 0x3F) + 0x80);
1011 }
1012
1013 if ($code >= 0xD800 && $code <= 0xDFFF) {
1014 /*
1015 The definition of UTF-8 prohibits encoding character numbers between
1016 U+D800 and U+DFFF, which are reserved for use with the UTF-16
1017 encoding form (as surrogate pairs) and do not directly represent characters.
1018 */
1019 return '';
1020 }
1021
1022 if ($code <= 0xFFFF) {
1023 return
1024 chr(($code >> 12) + 0xE0) .
1025 chr((($code >> 6) & 0x3F) + 0x80) .
1026 chr(($code & 0x3F) + 0x80);
1027 }
1028
1029 if ($code <= 0x10FFFF) {
1030 return
1031 chr(($code >> 18) + 0xF0) .
1032 chr((($code >> 12) & 0x3F) + 0x80) .
1033 chr((($code >> 6) & 0x3F) + 0x80) .
1034 chr(($code & 0x3F) + 0x80);
1035 }
1036
1037 /*
1038 Restricted the range of characters to 0000-10FFFF (the UTF-16 accessible range).
1039 */
1040
1041 return '';
1042 }
1043
1044 /**
1045 * Convert a string to a code point array
1046 * @param string $str
1047 * @param int $mode
1048 * @return array
1049 * @throws InvalidStringException
1050 */
1051 public static function getCodePointsFromString(string $str, int $mode = self::KEEP_CASE): array
1052 {
1053 // 0x00-0x7F
1054 // 0xC2-0xDF 0x80-0xBF
1055 // 0xE0-0xE0 0xA0-0xBF 0x80-0xBF
1056 // 0xE1-0xEC 0x80-0xBF 0x80-0xBF
1057 // 0xED-0xED 0x80-0x9F 0x80-0xBF
1058 // 0xEE-0xEF 0x80-0xBF 0x80-0xBF
1059 // 0xF0-0xF0 0x90-0xBF 0x80-0xBF 0x80-0xBF
1060 // 0xF1-0xF3 0x80-0xBF 0x80-0xBF 0x80-0xBF
1061 // 0xF4-0xF4 0x80-0x8F 0x80-0xBF 0x80-0xBF
1062
1063 $codes = [];
1064 $length = strlen($str);
1065 $mode = self::getMapByMode($mode);
1066
1067 $i = 0;
1068 while ($i < $length) {
1069 $ord0 = ord($str[$i++]);
1070
1071 if ($ord0 < 0x80) {
1072 $codes[] = $mode[$ord0] ?? $ord0;
1073 continue;
1074 }
1075
1076 if ($i === $length || $ord0 < 0xC2 || $ord0 > 0xF4) {
1077 throw new InvalidStringException($str, $i - 1);
1078 }
1079
1080 $ord1 = ord($str[$i++]);
1081
1082 if ($ord0 < 0xE0) {
1083 if ($ord1 < 0x80 || $ord1 >= 0xC0) {
1084 throw new InvalidStringException($str, $i - 1);
1085 }
1086
1087 $ord1 = ($ord0 - 0xC0) * 64 + $ord1 - 0x80;
1088 $codes[] = $mode[$ord1] ?? $ord1;
1089
1090 continue;
1091 }
1092
1093 if ($i === $length) {
1094 throw new InvalidStringException($str, $i - 1);
1095 }
1096
1097 $ord2 = ord($str[$i++]);
1098
1099 if ($ord0 < 0xF0) {
1100 if ($ord0 === 0xE0) {
1101 if ($ord1 < 0xA0 || $ord1 >= 0xC0) {
1102 throw new InvalidStringException($str, $i - 2);
1103 }
1104 } elseif ($ord0 === 0xED) {
1105 if ($ord1 < 0x80 || $ord1 >= 0xA0) {
1106 throw new InvalidStringException($str, $i - 2);
1107 }
1108 } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) {
1109 throw new InvalidStringException($str, $i - 2);
1110 }
1111
1112 if ($ord2 < 0x80 || $ord2 >= 0xC0) {
1113 throw new InvalidStringException($str, $i - 1);
1114 }
1115
1116 $ord2 = ($ord0 - 0xE0) * 0x1000 + ($ord1 - 0x80) * 64 + $ord2 - 0x80;
1117 $codes[] = $mode[$ord2] ?? $ord2;
1118
1119 continue;
1120 }
1121
1122 if ($i === $length) {
1123 throw new InvalidStringException($str, $i - 1);
1124 }
1125
1126 $ord3 = ord($str[$i++]);
1127
1128 if ($ord0 < 0xF5) {
1129 if ($ord0 === 0xF0) {
1130 if ($ord1 < 0x90 || $ord1 >= 0xC0) {
1131 throw new InvalidStringException($str, $i - 3);
1132 }
1133 } elseif ($ord0 === 0xF4) {
1134 if ($ord1 < 0x80 || $ord1 >= 0x90) {
1135 throw new InvalidStringException($str, $i - 3);
1136 }
1137 } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) {
1138 throw new InvalidStringException($str, $i - 3);
1139 }
1140
1141 if ($ord2 < 0x80 || $ord2 >= 0xC0) {
1142 throw new InvalidStringException($str, $i - 2);
1143 }
1144
1145 if ($ord3 < 0x80 || $ord3 >= 0xC0) {
1146 throw new InvalidStringException($str, $i - 1);
1147 }
1148
1149 $ord3 = ($ord0 - 0xF0) * 0x40000 + ($ord1 - 0x80) * 0x1000 + ($ord2 - 0x80) * 64 + $ord3 - 0x80;
1150 $codes[] = $mode[$ord3] ?? $ord3;
1151
1152 continue;
1153 }
1154
1155 throw new InvalidStringException($str, $i - 1);
1156 }
1157
1158 return $codes;
1159 }
1160
1161 /**
1162 * @param string $str
1163 * @return iterable
1164 *
1165 * The key represents the current char index
1166 * Value is a two element array
1167 * - first element is an integer representing the code point
1168 * - second element is an array of integers (length 1 to 4) representing bytes
1169 */
1170 public static function walkString(string $str): iterable
1171 {
1172 $i = 0;
1173 $length = strlen($str);
1174
1175 while ($i < $length) {
1176 $index = $i;
1177
1178 $ord0 = ord($str[$i++]);
1179
1180 if ($ord0 < 0x80) {
1181 yield $index => [
1182 $ord0,
1183 [$ord0]
1184 ];
1185 continue;
1186 }
1187
1188 if ($i === $length || $ord0 < 0xC2 || $ord0 > 0xF4) {
1189 throw new InvalidStringException($str, $i - 1);
1190 }
1191
1192 $ord1 = ord($str[$i++]);
1193
1194 if ($ord0 < 0xE0) {
1195 if ($ord1 < 0x80 || $ord1 >= 0xC0) {
1196 throw new InvalidStringException($str, $i - 1);
1197 }
1198
1199 yield $index => [
1200 ($ord0 - 0xC0) * 64 + $ord1 - 0x80,
1201 [$ord0, $ord1]
1202 ];
1203
1204 continue;
1205 }
1206
1207 if ($i === $length) {
1208 throw new InvalidStringException($str, $i - 1);
1209 }
1210
1211 $ord2 = ord($str[$i++]);
1212
1213 if ($ord0 < 0xF0) {
1214 if ($ord0 === 0xE0) {
1215 if ($ord1 < 0xA0 || $ord1 >= 0xC0) {
1216 throw new InvalidStringException($str, $i - 2);
1217 }
1218 } elseif ($ord0 === 0xED) {
1219 if ($ord1 < 0x80 || $ord1 >= 0xA0) {
1220 throw new InvalidStringException($str, $i - 2);
1221 }
1222 } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) {
1223 throw new InvalidStringException($str, $i - 2);
1224 }
1225
1226 if ($ord2 < 0x80 || $ord2 >= 0xC0) {
1227 throw new InvalidStringException($str, $i - 1);
1228 }
1229
1230 yield $index => [
1231 ($ord0 - 0xE0) * 0x1000 + ($ord1 - 0x80) * 64 + $ord2 - 0x80,
1232 [$ord0, $ord1, $ord2]
1233 ];
1234
1235 continue;
1236 }
1237
1238 if ($i === $length) {
1239 throw new InvalidStringException($str, $i - 1);
1240 }
1241
1242 $ord3 = ord($str[$i++]);
1243
1244 if ($ord0 < 0xF5) {
1245 if ($ord0 === 0xF0) {
1246 if ($ord1 < 0x90 || $ord1 >= 0xC0) {
1247 throw new InvalidStringException($str, $i - 3);
1248 }
1249 } elseif ($ord0 === 0xF4) {
1250 if ($ord1 < 0x80 || $ord1 >= 0x90) {
1251 throw new InvalidStringException($str, $i - 3);
1252 }
1253 } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) {
1254 throw new InvalidStringException($str, $i - 3);
1255 }
1256
1257 if ($ord2 < 0x80 || $ord2 >= 0xC0) {
1258 throw new InvalidStringException($str, $i - 2);
1259 }
1260
1261 if ($ord3 < 0x80 || $ord3 >= 0xC0) {
1262 throw new InvalidStringException($str, $i - 1);
1263 }
1264
1265 yield $index => [
1266 ($ord0 - 0xF0) * 0x40000 + ($ord1 - 0x80) * 0x1000 + ($ord2 - 0x80) * 64 + $ord3 - 0x80,
1267 [$ord0, $ord1, $ord2, $ord3]
1268 ];
1269
1270 continue;
1271 }
1272
1273 throw new InvalidStringException($str, $i - 1);
1274 }
1275 }
1276
1277 /**
1278 * Compute string length
1279 * @param string $str
1280 * @return int
1281 * @throws InvalidStringException
1282 */
1283 public static function getStringLength(string $str): int
1284 {
1285 $count = 0;
1286 $length = strlen($str);
1287
1288 $i = 0;
1289 while ($i < $length) {
1290 $ord0 = ord($str[$i++]);
1291
1292 if ($ord0 < 0x80) {
1293 $count++;
1294 continue;
1295 }
1296
1297 if ($i === $length || $ord0 < 0xC2 || $ord0 > 0xF4) {
1298 throw new InvalidStringException($str, $i - 1);
1299 }
1300
1301 $ord1 = ord($str[$i++]);
1302
1303 if ($ord0 < 0xE0) {
1304 if ($ord1 < 0x80 || $ord1 >= 0xC0) {
1305 throw new InvalidStringException($str, $i - 1);
1306 }
1307
1308 // $ord1 = ($ord0 - 0xC0) * 64 + $ord1 - 0x80;
1309 $count++;
1310
1311 continue;
1312 }
1313
1314 if ($i === $length) {
1315 throw new InvalidStringException($str, $i - 1);
1316 }
1317
1318 $ord2 = ord($str[$i++]);
1319
1320 if ($ord0 < 0xF0) {
1321 if ($ord0 === 0xE0) {
1322 if ($ord1 < 0xA0 || $ord1 >= 0xC0) {
1323 throw new InvalidStringException($str, $i - 2);
1324 }
1325 } elseif ($ord0 === 0xED) {
1326 if ($ord1 < 0x80 || $ord1 >= 0xA0) {
1327 throw new InvalidStringException($str, $i - 2);
1328 }
1329 } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) {
1330 throw new InvalidStringException($str, $i - 2);
1331 }
1332
1333 if ($ord2 < 0x80 || $ord2 >= 0xC0) {
1334 throw new InvalidStringException($str, $i - 1);
1335 }
1336
1337 // $ord2 = ($ord0 - 0xE0) * 0x1000 + ($ord1 - 0x80) * 64 + $ord2 - 0x80;
1338 $count++;
1339
1340 continue;
1341 }
1342
1343 if ($i === $length) {
1344 throw new InvalidStringException($str, $i - 1);
1345 }
1346
1347 $ord3 = ord($str[$i++]);
1348
1349 if ($ord0 < 0xF5) {
1350 if ($ord0 === 0xF0) {
1351 if ($ord1 < 0x90 || $ord1 >= 0xC0) {
1352 throw new InvalidStringException($str, $i - 3);
1353 }
1354 } elseif ($ord0 === 0xF4) {
1355 if ($ord1 < 0x80 || $ord1 >= 0x90) {
1356 throw new InvalidStringException($str, $i - 3);
1357 }
1358 } elseif ($ord1 < 0x80 || $ord1 >= 0xC0) {
1359 throw new InvalidStringException($str, $i - 3);
1360 }
1361
1362 if ($ord2 < 0x80 || $ord2 >= 0xC0) {
1363 throw new InvalidStringException($str, $i - 2);
1364 }
1365
1366 if ($ord3 < 0x80 || $ord3 >= 0xC0) {
1367 throw new InvalidStringException($str, $i - 1);
1368 }
1369
1370 // $ord3 = ($ord0 - 0xF0) * 0x40000 + ($ord1 - 0x80) * 0x1000 + ($ord2 - 0x80) * 64 + $ord3 - 0x80;
1371 $count++;
1372
1373 continue;
1374 }
1375
1376 throw new InvalidStringException($str, $i - 1);
1377 }
1378
1379 return $count;
1380 }
1381
1382 /**
1383 * Converts each code point to a char
1384 * @param array $codes
1385 * @param int $mode
1386 * @return array
1387 * @throws InvalidCodePointException
1388 */
1389 public static function getCharsFromCodePoints(array $codes, int $mode = self::KEEP_CASE): array
1390 {
1391 $mode = self::getMapByMode($mode);
1392
1393 foreach ($codes as &$code) {
1394 $char = self::getCharFromCodePoint($mode[$code] ?? $code);
1395 if ($char === '') {
1396 throw new InvalidCodePointException($code);
1397 } else {
1398 $code = $char;
1399 }
1400 }
1401
1402 return $codes;
1403 }
1404
1405 /**
1406 * @param string $str
1407 * @param int $mode
1408 * @return string[]
1409 */
1410 public static function getCharsFromString(string $str, int $mode = self::KEEP_CASE): array
1411 {
1412 return self::getCharsFromCodePoints(self::getCodePointsFromString($str), $mode);
1413 }
1414
1415 /**
1416 * Converts all code points to chars and returns the string
1417 * Invalid code points are ignored
1418 * @param array $codes
1419 * @param int $mode
1420 * @return string
1421 */
1422 public static function getStringFromCodePoints(array $codes, int $mode = self::KEEP_CASE): string
1423 {
1424 $str = '';
1425
1426 $mode = self::getMapByMode($mode);
1427
1428 foreach ($codes as $code) {
1429 if (isset($mode[$code])) {
1430 $code = $mode[$code];
1431 }
1432
1433 if ($code < 0x80) {
1434 $str .= chr($code);
1435 continue;
1436 }
1437
1438 if ($code < 0x800) {
1439 $str .= chr(($code >> 6) + 0xC0) . chr(($code & 0x3F) + 0x80);
1440 continue;
1441 }
1442
1443 if ($code >= 0xD800 && $code <= 0xDFFF) {
1444 continue;
1445 }
1446
1447 if ($code <= 0xFFFF) {
1448 $str .=
1449 chr(($code >> 12) + 0xE0) .
1450 chr((($code >> 6) & 0x3F) + 0x80) .
1451 chr(($code & 0x3F) + 0x80);
1452 continue;
1453 }
1454
1455 if ($code <= 0x10FFFF) {
1456 $str .=
1457 chr(($code >> 18) + 0xF0) .
1458 chr((($code >> 12) & 0x3F) + 0x80) .
1459 chr((($code >> 6) & 0x3F) + 0x80) .
1460 chr(($code & 0x3F) + 0x80);
1461 }
1462 }
1463
1464 return $str;
1465 }
1466
1467 /**
1468 * @param array $codes
1469 * @param int $mode
1470 * @return array
1471 */
1472 public static function getMappedCodePoints(array $codes, int $mode): array
1473 {
1474 if ($mode === self::KEEP_CASE) {
1475 return $codes;
1476 }
1477
1478 $mode = self::getMapByMode($mode);
1479
1480 if (empty($mode)) {
1481 return $codes;
1482 }
1483
1484 foreach ($codes as &$code) {
1485 $code = $mode[$code] ?? $code;
1486 }
1487
1488 return $codes;
1489 }
1490
1491 /**
1492 * Checks if a code point is valid
1493 * @param int $code
1494 * @return bool
1495 */
1496 public static function isValidCodePoint(int $code): bool
1497 {
1498 if ($code < 0 || $code > 0x10FFFF) {
1499 return false;
1500 }
1501
1502 return $code < 0xD800 || $code > 0xDFFF;
1503 }
1504
1505 /**
1506 * @param int $mode
1507 * @return int[]
1508 */
1509 private function getMappedCodes(int $mode): array
1510 {
1511 if ($mode === self::KEEP_CASE || ($this->cache['i' . $mode] ?? false)) {
1512 return $this->codes;
1513 }
1514
1515 $key = 'm' . $mode;
1516
1517 if (!isset($this->cache[$key])) {
1518 $this->cache[$key] = self::getMappedCodePoints($this->codes, $mode);
1519 }
1520
1521 return $this->cache[$key];
1522 }
1523
1524 /**
1525 * @param int $mode
1526 * @return bool
1527 */
1528 private function isCase(int $mode): bool
1529 {
1530 $key = 'i' . $mode;
1531
1532 if (!isset($this->cache[$key])) {
1533 $list = self::getMapByMode($mode);
1534 foreach ($this->codes as $code) {
1535 if (isset($list[$code])) {
1536 return $this->cache[$key] = false;
1537 }
1538 }
1539
1540 return $this->cache[$key] = true;
1541 }
1542
1543 return $this->cache[$key];
1544 }
1545
1546 /**
1547 * @param int[] $codes
1548 * @param int[] $text
1549 * @param int $offset
1550 * @return int
1551 */
1552 private function doIndexOf(array $codes, array $text, int $offset = 0): int
1553 {
1554 $len = count($text);
1555
1556 for ($i = $offset, $last = count($codes) - $len; $i <= $last; $i++) {
1557 $match = true;
1558
1559 for ($j = 0; $j < $len; $j++) {
1560 if ($codes[$i + $j] !== $text[$j]) {
1561 $match = false;
1562 break;
1563 }
1564 }
1565
1566 if ($match) {
1567 return $i;
1568 }
1569 }
1570
1571 return -1;
1572 }
1573
1574 /**
1575 * @param string|self|int[]|string[] $mask
1576 * @param bool $left
1577 * @param bool $right
1578 * @return static
1579 */
1580 private function doTrim($mask, bool $left, bool $right): self
1581 {
1582 if ($this->length === 0) {
1583 return clone $this;
1584 }
1585
1586 $mask = self::resolveCodePoints($mask);
1587
1588 if (empty($mask)) {
1589 return clone $this;
1590 }
1591
1592 $codes = $this->codes;
1593
1594 if ($left) {
1595 while (in_array($codes[0], $mask, true)) {
1596 array_shift($codes);
1597 if (empty($codes)) {
1598 return new static();
1599 }
1600 }
1601 }
1602
1603 if ($right) {
1604 $last = count($codes) - 1;
1605 while (in_array($codes[$last], $mask, true)) {
1606 array_pop($codes);
1607 if (--$last < 0) {
1608 return new static();
1609 }
1610 }
1611 }
1612
1613 return new static($codes);
1614 }
1615
1616
1617 /**
1618 * @param string|self|int[]|string[] $text
1619 * @param int $mode
1620 * @return array
1621 */
1622 private static function resolveCodePoints($text, int $mode = self::KEEP_CASE): array
1623 {
1624 if ($text instanceof self) {
1625 return $text->getMappedCodes($mode);
1626 }
1627
1628 if (is_string($text)) {
1629 return self::getCodePointsFromString($text, $mode);
1630 }
1631
1632 if ($text && is_array($text) && is_int($text[0])) {
1633 // assume code point array
1634 return self::getMappedCodePoints($text, $mode);
1635 }
1636
1637 return [];
1638 }
1639
1640 /**
1641 * @param self|string|int|string[]|int[] $text
1642 * @param int $invalid
1643 * @return int
1644 */
1645 private static function resolveFirstCodePoint($text, int $invalid = -1): int
1646 {
1647 if ($text instanceof self) {
1648 return $text->length === 0 ? $invalid : $text->codes[0];
1649 }
1650
1651 if (is_array($text)) {
1652 if (empty($text)) {
1653 return $invalid;
1654 }
1655 $text = reset($text);
1656 }
1657
1658 if (is_string($text)) {
1659 if (isset($text[4])) {
1660 $text = substr($text, 0, 4);
1661 }
1662 return self::getCodePointsFromString($text)[0] ?? $invalid;
1663 }
1664
1665 if (is_int($text)) {
1666 return self::isValidCodePoint($text) ? $text : $invalid;
1667 }
1668
1669 return $invalid;
1670 }
1671
1672 /**
1673 * @param int $mode
1674 * @return int[]
1675 */
1676 private static function getMapByMode(int $mode): array
1677 {
1678 if (isset(self::$maps[$mode])) {
1679 return self::$maps[$mode];
1680 }
1681
1682 switch ($mode) {
1683 case self::LOWER_CASE:
1684 $file = 'lower';
1685 break;
1686 case self::UPPER_CASE:
1687 $file = 'upper';
1688 break;
1689 case self::ASCII_CONV:
1690 $file = 'ascii';
1691 break;
1692 case self::FOLD_CASE:
1693 $file = 'fold';
1694 break;
1695 default:
1696 return [];
1697 }
1698
1699 /** @noinspection PhpIncludeInspection */
1700 return self::$maps[$mode] = include(__DIR__ . "/../res/{$file}.php");
1701 }
1702 }
1703