1 | <?php |
||
2 | declare(strict_types = 1); |
||
3 | |||
4 | namespace Innmind\Immutable; |
||
5 | |||
6 | use Innmind\Immutable\Exception\InvalidRegex; |
||
7 | |||
8 | /** |
||
9 | * @psalm-immutable |
||
10 | */ |
||
11 | final class Str |
||
12 | { |
||
13 | private string $value; |
||
14 | private string $encoding; |
||
15 | |||
16 | private function __construct(string $value, string $encoding = null) |
||
17 | { |
||
18 | $this->value = $value; |
||
19 | /** |
||
20 | * @psalm-suppress ImpureFunctionCall |
||
21 | * @var string |
||
22 | */ |
||
23 | $this->encoding = $encoding ?? \mb_internal_encoding(); |
||
24 | } |
||
25 | |||
26 | 406 | /** |
|
27 | * @psalm-pure |
||
28 | 406 | */ |
|
29 | 406 | public static function of(string $value, string $encoding = null): self |
|
30 | 406 | { |
|
31 | return new self($value, $encoding); |
||
32 | 406 | } |
|
33 | |||
34 | 406 | /** |
|
35 | * Concatenate all elements with the given separator |
||
36 | * |
||
37 | * @param Set<string>|Sequence<string> $structure |
||
38 | */ |
||
39 | public function join(Set|Sequence $structure): self |
||
40 | 404 | { |
|
41 | return new self( |
||
42 | 404 | \implode($this->value, $structure->toList()), |
|
43 | $this->encoding, |
||
44 | ); |
||
45 | 384 | } |
|
46 | |||
47 | 384 | public function toString(): string |
|
48 | { |
||
49 | 380 | return $this->value; |
|
50 | } |
||
51 | |||
52 | 384 | public function encoding(): self |
|
53 | { |
||
54 | return new self($this->encoding); |
||
55 | 16 | } |
|
56 | |||
57 | 16 | public function toEncoding(string $encoding): self |
|
58 | { |
||
59 | return new self($this->value, $encoding); |
||
60 | } |
||
61 | |||
62 | /** |
||
63 | * Split the string into a collection of ones |
||
64 | * |
||
65 | 20 | * @return Sequence<self> |
|
66 | */ |
||
67 | 20 | public function split(string $delimiter = null): Sequence |
|
68 | 3 | { |
|
69 | if (\is_null($delimiter) || $delimiter === '') { |
||
70 | return $this->chunk(); |
||
71 | 18 | } |
|
72 | |||
73 | 18 | $parts = \explode($delimiter, $this->value); |
|
74 | /** @var Sequence<self> */ |
||
75 | 18 | $sequence = Sequence::of(); |
|
76 | 18 | ||
77 | foreach ($parts as $part) { |
||
78 | $sequence = ($sequence)(new self($part, $this->encoding)); |
||
79 | 18 | } |
|
80 | |||
81 | return $sequence; |
||
82 | } |
||
83 | |||
84 | /** |
||
85 | * Returns a collection of the string splitted by the given chunk size |
||
86 | * |
||
87 | 8 | * @param positive-int $size |
|
0 ignored issues
–
show
Documentation
Bug
introduced
by
![]() |
|||
88 | * |
||
89 | * @return Sequence<self> |
||
90 | 8 | */ |
|
91 | public function chunk(int $size = 1): Sequence |
||
92 | 8 | { |
|
93 | /** @var Sequence<self> */ |
||
94 | 8 | $sequence = Sequence::of(); |
|
95 | 8 | $parts = \mb_str_split($this->value, $size, $this->encoding); |
|
96 | |||
97 | foreach ($parts as $value) { |
||
98 | 8 | $sequence = ($sequence)(new self($value, $this->encoding)); |
|
99 | } |
||
100 | |||
101 | return $sequence; |
||
102 | } |
||
103 | |||
104 | /** |
||
105 | * Returns the position of the first occurence of the string |
||
106 | 367 | * |
|
107 | * @return Maybe<int> |
||
108 | 367 | */ |
|
109 | public function position(string $needle, int $offset = 0): Maybe |
||
110 | 367 | { |
|
111 | 365 | $position = \mb_strpos($this->value, $needle, $offset, $this->encoding); |
|
112 | 365 | ||
113 | 365 | if ($position === false) { |
|
114 | /** @var Maybe<int> */ |
||
115 | return Maybe::nothing(); |
||
116 | } |
||
117 | 19 | ||
118 | return Maybe::just($position); |
||
119 | } |
||
120 | |||
121 | /** |
||
122 | * Replace all occurences of the search string with the replacement one |
||
123 | 2 | */ |
|
124 | public function replace(string $search, string $replacement): self |
||
125 | 2 | { |
|
126 | 1 | if (!$this->contains($search)) { |
|
127 | return $this; |
||
128 | } |
||
129 | |||
130 | $parts = $this |
||
131 | ->split($search) |
||
132 | ->map(static fn($v) => $v->toString()); |
||
133 | |||
134 | 2 | return self::of($replacement, $this->encoding)->join($parts); |
|
135 | 2 | } |
|
136 | |||
137 | 2 | /** |
|
138 | * Return the string in upper case |
||
139 | */ |
||
140 | public function toUpper(): self |
||
141 | { |
||
142 | return new self(\mb_strtoupper($this->value), $this->encoding); |
||
143 | } |
||
144 | |||
145 | 3 | /** |
|
146 | * Return the string in lower case |
||
147 | 3 | */ |
|
148 | public function toLower(): self |
||
149 | 3 | { |
|
150 | 1 | return new self(\mb_strtolower($this->value), $this->encoding); |
|
151 | 1 | } |
|
152 | 1 | ||
153 | /** |
||
154 | * Return the string length |
||
155 | */ |
||
156 | 2 | public function length(): int |
|
157 | { |
||
158 | return \mb_strlen($this->value, $this->encoding); |
||
159 | } |
||
160 | |||
161 | public function empty(): bool |
||
162 | 3 | { |
|
163 | return $this->value === ''; |
||
164 | 3 | } |
|
165 | |||
166 | /** |
||
167 | * Reverse the string |
||
168 | */ |
||
169 | public function reverse(): self |
||
170 | 3 | { |
|
171 | $parts = $this |
||
172 | 3 | ->chunk() |
|
173 | ->reverse() |
||
174 | ->map(static fn($v) => $v->toString()); |
||
175 | |||
176 | return self::of('', $this->encoding)->join($parts); |
||
177 | } |
||
178 | 5 | ||
179 | /** |
||
180 | 5 | * Pad to the right |
|
181 | * |
||
182 | * @param positive-int $length |
||
0 ignored issues
–
show
|
|||
183 | 12 | */ |
|
184 | public function rightPad(int $length, string $character = ' '): self |
||
185 | 12 | { |
|
186 | return $this->pad($length, $character, \STR_PAD_RIGHT); |
||
187 | } |
||
188 | |||
189 | /** |
||
190 | * Pad to the left |
||
191 | 2 | * |
|
192 | * @param positive-int $length |
||
0 ignored issues
–
show
|
|||
193 | */ |
||
194 | public function leftPad(int $length, string $character = ' '): self |
||
195 | { |
||
196 | return $this->pad($length, $character, \STR_PAD_LEFT); |
||
197 | } |
||
198 | 2 | ||
199 | 2 | /** |
|
200 | 2 | * Pad both sides |
|
201 | * |
||
202 | 2 | * @param positive-int $length |
|
0 ignored issues
–
show
|
|||
203 | */ |
||
204 | public function uniPad(int $length, string $character = ' '): self |
||
205 | { |
||
206 | return $this->pad($length, $character, \STR_PAD_BOTH); |
||
207 | } |
||
208 | 1 | ||
209 | /** |
||
210 | 1 | * Repeat the string n times |
|
211 | * |
||
212 | * @param positive-int $repeat |
||
0 ignored issues
–
show
|
|||
213 | */ |
||
214 | public function repeat(int $repeat): self |
||
215 | { |
||
216 | 1 | return new self(\str_repeat($this->value, $repeat), $this->encoding); |
|
217 | } |
||
218 | 1 | ||
219 | public function stripSlashes(): self |
||
220 | { |
||
221 | return new self(\stripslashes($this->value), $this->encoding); |
||
222 | } |
||
223 | |||
224 | 1 | /** |
|
225 | * Strip C-like slashes |
||
226 | 1 | */ |
|
227 | public function stripCSlashes(): self |
||
228 | { |
||
229 | return new self(\stripcslashes($this->value), $this->encoding); |
||
230 | } |
||
231 | |||
232 | 1 | /** |
|
233 | * Return the word count |
||
234 | 1 | */ |
|
235 | 1 | public function wordCount(string $charlist = ''): int |
|
236 | { |
||
237 | 1 | return \str_word_count( |
|
238 | 1 | $this->value, |
|
239 | 1 | 0, |
|
240 | 1 | $charlist, |
|
241 | 1 | ); |
|
242 | } |
||
243 | |||
244 | /** |
||
245 | 1 | * Return the collection of words |
|
246 | * |
||
247 | * @return Map<int, self> |
||
248 | */ |
||
249 | public function words(string $charlist = ''): Map |
||
250 | { |
||
251 | 1 | /** @var list<string> */ |
|
252 | $words = \str_word_count($this->value, 2, $charlist); |
||
253 | 1 | /** @var Map<int, self> */ |
|
254 | $map = Map::of(); |
||
255 | |||
256 | foreach ($words as $position => $word) { |
||
257 | $map = ($map)($position, new self($word, $this->encoding)); |
||
258 | } |
||
259 | 2 | ||
260 | return $map; |
||
261 | } |
||
262 | 2 | ||
263 | /** |
||
264 | 2 | * Split the string using a regular expression |
|
265 | 2 | * |
|
266 | * @return Sequence<self> |
||
267 | 2 | */ |
|
268 | public function pregSplit(string $regex, int $limit = -1): Sequence |
||
269 | 2 | { |
|
270 | $strings = \preg_split($regex, $this->value, $limit); |
||
271 | /** @var Sequence<self> */ |
||
272 | 1 | $sequence = Sequence::of(); |
|
273 | |||
274 | 1 | foreach ($strings as $string) { |
|
275 | $sequence = ($sequence)(new self($string, $this->encoding)); |
||
276 | } |
||
277 | |||
278 | return $sequence; |
||
279 | } |
||
280 | 1 | ||
281 | /** |
||
282 | 1 | * Check if the string match the given regular expression |
|
283 | * |
||
284 | * @throws InvalidRegex If the regex failed |
||
285 | */ |
||
286 | public function matches(string $regex): bool |
||
287 | { |
||
288 | 1 | return RegExp::of($regex)->matches($this); |
|
289 | } |
||
290 | 1 | ||
291 | 1 | /** |
|
292 | 1 | * Return a collection of the elements matching the regex |
|
293 | 1 | * |
|
294 | * @throws InvalidRegex If the regex failed |
||
295 | * |
||
296 | * @return Map<int|string, self> |
||
297 | */ |
||
298 | public function capture(string $regex): Map |
||
299 | { |
||
300 | return RegExp::of($regex)->capture($this); |
||
301 | } |
||
302 | 1 | ||
303 | /** |
||
304 | * Replace part of the string by using a regular expression |
||
305 | 1 | * |
|
306 | * @throws InvalidRegex If the regex failed |
||
307 | 1 | */ |
|
308 | public function pregReplace( |
||
309 | 1 | string $regex, |
|
310 | 1 | string $replacement, |
|
311 | int $limit = -1, |
||
312 | ): self { |
||
313 | 1 | $value = \preg_replace( |
|
314 | $regex, |
||
315 | $replacement, |
||
316 | $this->value, |
||
317 | $limit, |
||
318 | ); |
||
319 | |||
320 | if ($value === null) { |
||
321 | 2 | /** @psalm-suppress ImpureFunctionCall */ |
|
322 | throw new InvalidRegex('', \preg_last_error()); |
||
323 | 2 | } |
|
324 | |||
325 | 2 | return new self($value, $this->encoding); |
|
326 | } |
||
327 | 2 | ||
328 | 2 | /** |
|
329 | * Return part of the string |
||
330 | */ |
||
331 | 2 | public function substring(int $start, int $length = null): self |
|
332 | { |
||
333 | if ($this->empty()) { |
||
334 | return $this; |
||
335 | } |
||
336 | |||
337 | $sub = \mb_substr($this->value, $start, $length, $this->encoding); |
||
338 | |||
339 | 2 | return new self($sub, $this->encoding); |
|
340 | } |
||
341 | 2 | ||
342 | /** |
||
343 | * @param positive-int $size |
||
0 ignored issues
–
show
|
|||
344 | */ |
||
345 | public function take(int $size): self |
||
346 | { |
||
347 | return $this->substring(0, $size); |
||
348 | } |
||
349 | |||
350 | /** |
||
351 | 3 | * @param positive-int $size |
|
0 ignored issues
–
show
|
|||
352 | */ |
||
353 | 3 | public function takeEnd(int $size): self |
|
354 | { |
||
355 | return $this->substring(-$size); |
||
356 | } |
||
357 | |||
358 | /** |
||
359 | * @param positive-int $size |
||
0 ignored issues
–
show
|
|||
360 | */ |
||
361 | 1 | public function drop(int $size): self |
|
362 | { |
||
363 | return $this->substring($size); |
||
364 | } |
||
365 | |||
366 | 1 | /** |
|
367 | 1 | * @param positive-int $size |
|
0 ignored issues
–
show
|
|||
368 | 1 | */ |
|
369 | 1 | public function dropEnd(int $size): self |
|
370 | 1 | { |
|
371 | return $this->substring(0, $this->length() - $size); |
||
372 | } |
||
373 | 1 | ||
374 | /** |
||
375 | * Return a formatted string |
||
376 | */ |
||
377 | 1 | public function sprintf(string ...$values): self |
|
378 | { |
||
379 | return new self(\sprintf($this->value, ...$values), $this->encoding); |
||
380 | } |
||
381 | |||
382 | /** |
||
383 | 12 | * Return the string with the first letter as uppercase |
|
384 | */ |
||
385 | 12 | public function ucfirst(): self |
|
386 | 1 | { |
|
387 | return $this |
||
388 | ->substring(0, 1) |
||
389 | 12 | ->toUpper() |
|
390 | ->append($this->substring(1)->toString()); |
||
391 | 12 | } |
|
392 | |||
393 | /** |
||
394 | 1 | * Return the string with the first letter as lowercase |
|
395 | */ |
||
396 | 1 | public function lcfirst(): self |
|
397 | { |
||
398 | return $this |
||
399 | 2 | ->substring(0, 1) |
|
400 | ->toLower() |
||
401 | 2 | ->append($this->substring(1)->toString()); |
|
402 | } |
||
403 | |||
404 | 2 | /** |
|
405 | * Return a camelCase representation of the string |
||
406 | 2 | */ |
|
407 | public function camelize(): self |
||
408 | { |
||
409 | 1 | $words = $this |
|
410 | ->pregSplit('/_| /') |
||
411 | 1 | ->map(static fn(self $part) => $part->ucfirst()->toString()); |
|
412 | |||
413 | return self::of('', $this->encoding) |
||
414 | ->join($words) |
||
415 | ->lcfirst(); |
||
416 | } |
||
417 | 1 | ||
418 | /** |
||
419 | 1 | * Append a string at the end of the current one |
|
420 | */ |
||
421 | public function append(string $string): self |
||
422 | { |
||
423 | return new self($this->value.$string, $this->encoding); |
||
424 | } |
||
425 | 2 | ||
426 | /** |
||
427 | * Prepend a string at the beginning of the current one |
||
428 | 2 | */ |
|
429 | 2 | public function prepend(string $string): self |
|
430 | 2 | { |
|
431 | return new self($string.$this->value, $this->encoding); |
||
432 | } |
||
433 | |||
434 | /** |
||
435 | * Check if the 2 strings are equal |
||
436 | 2 | */ |
|
437 | public function equals(self $string): bool |
||
438 | { |
||
439 | 2 | return $this->toString() === $string->toString(); |
|
440 | 2 | } |
|
441 | 2 | ||
442 | /** |
||
443 | * Trim the string |
||
444 | */ |
||
445 | public function trim(string $mask = null): self |
||
446 | { |
||
447 | 1 | return new self( |
|
448 | $mask === null ? \trim($this->value) : \trim($this->value, $mask), |
||
449 | $this->encoding, |
||
450 | ); |
||
451 | } |
||
452 | |||
453 | /** |
||
454 | 1 | * Trim the right side of the string |
|
455 | */ |
||
456 | 1 | public function rightTrim(string $mask = null): self |
|
457 | 1 | { |
|
458 | 1 | return new self( |
|
459 | $mask === null ? \rtrim($this->value) : \rtrim($this->value, $mask), |
||
460 | 1 | $this->encoding, |
|
461 | 1 | ); |
|
462 | 1 | } |
|
463 | |||
464 | /** |
||
465 | * Trim the left side of the string |
||
466 | */ |
||
467 | public function leftTrim(string $mask = null): self |
||
468 | 4 | { |
|
469 | return new self( |
||
470 | 4 | $mask === null ? \ltrim($this->value) : \ltrim($this->value, $mask), |
|
471 | $this->encoding, |
||
472 | ); |
||
473 | } |
||
474 | |||
475 | /** |
||
476 | 1 | * Check if the given string is present in the current one |
|
477 | */ |
||
478 | 1 | public function contains(string $value): bool |
|
479 | { |
||
480 | return \mb_strpos($this->value, $value, 0, $this->encoding) !== false; |
||
481 | } |
||
482 | |||
483 | /** |
||
484 | 1 | * Check if the current string starts with the given string |
|
485 | */ |
||
486 | 1 | public function startsWith(string $value): bool |
|
487 | { |
||
488 | if ($value === '') { |
||
489 | return true; |
||
490 | } |
||
491 | |||
492 | 1 | return \mb_strpos($this->value, $value, 0, $this->encoding) === 0; |
|
493 | } |
||
494 | 1 | ||
495 | 1 | /** |
|
496 | 1 | * Check if the current string ends with the given string |
|
497 | */ |
||
498 | public function endsWith(string $value): bool |
||
499 | { |
||
500 | if ($value === '') { |
||
501 | return true; |
||
502 | } |
||
503 | 1 | ||
504 | /** @var positive-int */ |
||
505 | 1 | $length = self::of($value, $this->encoding)->length(); |
|
506 | 1 | ||
507 | 1 | return $this->takeEnd($length)->toString() === $value; |
|
508 | } |
||
509 | |||
510 | /** |
||
511 | * Quote regular expression characters |
||
512 | */ |
||
513 | public function pregQuote(string $delimiter = ''): self |
||
514 | 1 | { |
|
515 | return new self(\preg_quote($this->value, $delimiter), $this->encoding); |
||
516 | 1 | } |
|
517 | 1 | ||
518 | 1 | /** |
|
519 | * @param callable(string, string): string $map Second string is the encoding |
||
520 | */ |
||
521 | public function map(callable $map): self |
||
522 | { |
||
523 | return new self($map($this->value, $this->encoding), $this->encoding); |
||
524 | } |
||
525 | 364 | ||
526 | /** |
||
527 | * @param callable(string, string): self $map Second string is the encoding |
||
528 | 364 | */ |
|
529 | public function flatMap(callable $map): self |
||
530 | 17 | { |
|
531 | 363 | return $map($this->value, $this->encoding); |
|
532 | 363 | } |
|
533 | |||
534 | /** |
||
535 | * Pad the string |
||
536 | */ |
||
537 | private function pad(int $length, string $character, int $direction): self |
||
538 | { |
||
539 | 363 | return new self(\str_pad( |
|
540 | $this->value, |
||
541 | 363 | $length, |
|
542 | 1 | $character, |
|
543 | $direction, |
||
544 | ), $this->encoding); |
||
545 | } |
||
546 | } |
||
547 |