Innmind /
Immutable
| 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
Loading history...
|
|||
| 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 |