Completed
Push — develop ( f11ef2...d41b65 )
by David
06:01 queued 11s
created

AbstractString::split()   B

Complexity

Conditions 10
Paths 65

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 65
nop 3
dl 0
loc 44
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\String;
13
14
use Symfony\Component\String\Exception\ExceptionInterface;
15
use Symfony\Component\String\Exception\InvalidArgumentException;
16
use Symfony\Component\String\Exception\RuntimeException;
17
18
/**
19
 * Represents a string of abstract characters.
20
 *
21
 * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters).
22
 * This class is the abstract type to use as a type-hint when the logic you want to
23
 * implement doesn't care about the exact variant it deals with.
24
 *
25
 * @author Nicolas Grekas <[email protected]>
26
 * @author Hugo Hamon <[email protected]>
27
 *
28
 * @throws ExceptionInterface
29
 */
30
abstract class AbstractString implements \Stringable, \JsonSerializable
31
{
32
    public const PREG_PATTERN_ORDER = PREG_PATTERN_ORDER;
33
    public const PREG_SET_ORDER = PREG_SET_ORDER;
34
    public const PREG_OFFSET_CAPTURE = PREG_OFFSET_CAPTURE;
35
    public const PREG_UNMATCHED_AS_NULL = PREG_UNMATCHED_AS_NULL;
36
37
    public const PREG_SPLIT = 0;
38
    public const PREG_SPLIT_NO_EMPTY = PREG_SPLIT_NO_EMPTY;
39
    public const PREG_SPLIT_DELIM_CAPTURE = PREG_SPLIT_DELIM_CAPTURE;
40
    public const PREG_SPLIT_OFFSET_CAPTURE = PREG_SPLIT_OFFSET_CAPTURE;
41
42
    protected $string = '';
43
    protected $ignoreCase = false;
44
45
    abstract public function __construct(string $string = '');
46
47
    /**
48
     * Unwraps instances of AbstractString back to strings.
49
     *
50
     * @return string[]|array
51
     */
52
    public static function unwrap(array $values): array
53
    {
54
        foreach ($values as $k => $v) {
55
            if ($v instanceof self) {
56
                $values[$k] = $v->__toString();
57
            } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) {
58
                $values[$k] = $v;
59
            }
60
        }
61
62
        return $values;
63
    }
64
65
    /**
66
     * Wraps (and normalizes) strings in instances of AbstractString.
67
     *
68
     * @return static[]|array
69
     */
70
    public static function wrap(array $values): array
71
    {
72
        $i = 0;
73
        $keys = null;
74
75
        foreach ($values as $k => $v) {
76
            if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) {
77
                $keys = $keys ?? array_keys($values);
78
                $keys[$i] = $j;
79
            }
80
81
            if (\is_string($v)) {
82
                $values[$k] = new static($v);
83
            } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) {
84
                $values[$k] = $v;
85
            }
86
87
            ++$i;
88
        }
89
90
        return null !== $keys ? array_combine($keys, $values) : $values;
91
    }
92
93
    /**
94
     * @param string|string[] $needle
95
     *
96
     * @return static
97
     */
98
    public function after($needle, bool $includeNeedle = false, int $offset = 0): self
99
    {
100
        $str = clone $this;
101
        $str->string = '';
102
        $i = PHP_INT_MAX;
103
104
        foreach ((array) $needle as $n) {
105
            $n = (string) $n;
106
            $j = $this->indexOf($n, $offset);
107
108
            if (null !== $j && $j < $i) {
109
                $i = $j;
110
                $str->string = $n;
111
            }
112
        }
113
114
        if (PHP_INT_MAX === $i) {
115
            return $str;
116
        }
117
118
        if (!$includeNeedle) {
119
            $i += $str->length();
120
        }
121
122
        return $this->slice($i);
123
    }
124
125
    /**
126
     * @param string|string[] $needle
127
     *
128
     * @return static
129
     */
130
    public function afterLast($needle, bool $includeNeedle = false, int $offset = 0): self
131
    {
132
        $str = clone $this;
133
        $str->string = '';
134
        $i = null;
135
136
        foreach ((array) $needle as $n) {
137
            $n = (string) $n;
138
            $j = $this->indexOfLast($n, $offset);
139
140
            if (null !== $j && $j >= $i) {
141
                $i = $offset = $j;
142
                $str->string = $n;
143
            }
144
        }
145
146
        if (null === $i) {
147
            return $str;
148
        }
149
150
        if (!$includeNeedle) {
151
            $i += $str->length();
152
        }
153
154
        return $this->slice($i);
155
    }
156
157
    /**
158
     * @return static
159
     */
160
    abstract public function append(string ...$suffix): self;
161
162
    /**
163
     * @param string|string[] $needle
164
     *
165
     * @return static
166
     */
167
    public function before($needle, bool $includeNeedle = false, int $offset = 0): self
168
    {
169
        $str = clone $this;
170
        $str->string = '';
171
        $i = PHP_INT_MAX;
172
173
        foreach ((array) $needle as $n) {
174
            $n = (string) $n;
175
            $j = $this->indexOf($n, $offset);
176
177
            if (null !== $j && $j < $i) {
178
                $i = $j;
179
                $str->string = $n;
180
            }
181
        }
182
183
        if (PHP_INT_MAX === $i) {
184
            return $str;
185
        }
186
187
        if ($includeNeedle) {
188
            $i += $str->length();
189
        }
190
191
        return $this->slice(0, $i);
192
    }
193
194
    /**
195
     * @param string|string[] $needle
196
     *
197
     * @return static
198
     */
199
    public function beforeLast($needle, bool $includeNeedle = false, int $offset = 0): self
200
    {
201
        $str = clone $this;
202
        $str->string = '';
203
        $i = null;
204
205
        foreach ((array) $needle as $n) {
206
            $n = (string) $n;
207
            $j = $this->indexOfLast($n, $offset);
208
209
            if (null !== $j && $j >= $i) {
210
                $i = $offset = $j;
211
                $str->string = $n;
212
            }
213
        }
214
215
        if (null === $i) {
216
            return $str;
217
        }
218
219
        if ($includeNeedle) {
220
            $i += $str->length();
221
        }
222
223
        return $this->slice(0, $i);
224
    }
225
226
    /**
227
     * @return int[]
228
     */
229
    public function bytesAt(int $offset): array
230
    {
231
        $str = $this->slice($offset, 1);
232
233
        return '' === $str->string ? [] : array_values(unpack('C*', $str->string));
234
    }
235
236
    /**
237
     * @return static
238
     */
239
    abstract public function camel(): self;
240
241
    /**
242
     * @return static[]
243
     */
244
    abstract public function chunk(int $length = 1): array;
245
246
    /**
247
     * @return static
248
     */
249
    public function collapseWhitespace(): self
250
    {
251
        $str = clone $this;
252
        $str->string = trim(preg_replace('/(?:\s{2,}+|[^\S ])/', ' ', $str->string));
253
254
        return $str;
255
    }
256
257
    /**
258
     * @param string|string[] $needle
259
     */
260
    public function containsAny($needle): bool
261
    {
262
        return null !== $this->indexOf($needle);
263
    }
264
265
    /**
266
     * @param string|string[] $suffix
267
     */
268
    public function endsWith($suffix): bool
269
    {
270
        if (!\is_array($suffix) && !$suffix instanceof \Traversable) {
271
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
272
        }
273
274
        foreach ($suffix as $s) {
275
            if ($this->endsWith((string) $s)) {
276
                return true;
277
            }
278
        }
279
280
        return false;
281
    }
282
283
    /**
284
     * @return static
285
     */
286
    public function ensureEnd(string $suffix): self
287
    {
288
        if (!$this->endsWith($suffix)) {
289
            return $this->append($suffix);
290
        }
291
292
        $suffix = preg_quote($suffix);
293
        $regex = '{('.$suffix.')(?:'.$suffix.')++$}D';
294
295
        return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1');
296
    }
297
298
    /**
299
     * @return static
300
     */
301
    public function ensureStart(string $prefix): self
302
    {
303
        $prefix = new static($prefix);
304
305
        if (!$this->startsWith($prefix)) {
306
            return $this->prepend($prefix);
307
        }
308
309
        $str = clone $this;
310
        $i = $prefixLen = $prefix->length();
311
312
        while ($this->indexOf($prefix, $i) === $i) {
313
            $str = $str->slice($prefixLen);
314
            $i += $prefixLen;
315
        }
316
317
        return $str;
318
    }
319
320
    /**
321
     * @param string|string[] $string
322
     */
323
    public function equalsTo($string): bool
324
    {
325
        if (!\is_array($string) && !$string instanceof \Traversable) {
326
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
327
        }
328
329
        foreach ($string as $s) {
330
            if ($this->equalsTo((string) $s)) {
331
                return true;
332
            }
333
        }
334
335
        return false;
336
    }
337
338
    /**
339
     * @return static
340
     */
341
    abstract public function folded(): self;
342
343
    /**
344
     * @return static
345
     */
346
    public function ignoreCase(): self
347
    {
348
        $str = clone $this;
349
        $str->ignoreCase = true;
350
351
        return $str;
352
    }
353
354
    /**
355
     * @param string|string[] $needle
356
     */
357
    public function indexOf($needle, int $offset = 0): ?int
358
    {
359
        if (!\is_array($needle) && !$needle instanceof \Traversable) {
360
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
361
        }
362
363
        $i = PHP_INT_MAX;
364
365
        foreach ($needle as $n) {
366
            $j = $this->indexOf((string) $n, $offset);
367
368
            if (null !== $j && $j < $i) {
369
                $i = $j;
370
            }
371
        }
372
373
        return PHP_INT_MAX === $i ? null : $i;
374
    }
375
376
    /**
377
     * @param string|string[] $needle
378
     */
379
    public function indexOfLast($needle, int $offset = 0): ?int
380
    {
381
        if (!\is_array($needle) && !$needle instanceof \Traversable) {
382
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
383
        }
384
385
        $i = null;
386
387
        foreach ($needle as $n) {
388
            $j = $this->indexOfLast((string) $n, $offset);
389
390
            if (null !== $j && $j >= $i) {
391
                $i = $offset = $j;
392
            }
393
        }
394
395
        return $i;
396
    }
397
398
    public function isEmpty(): bool
399
    {
400
        return '' === $this->string;
401
    }
402
403
    /**
404
     * @return static
405
     */
406
    abstract public function join(array $strings, string $lastGlue = null): self;
407
408
    public function jsonSerialize(): string
409
    {
410
        return $this->string;
411
    }
412
413
    abstract public function length(): int;
414
415
    /**
416
     * @return static
417
     */
418
    abstract public function lower(): self;
419
420
    /**
421
     * Matches the string using a regular expression.
422
     *
423
     * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression.
424
     *
425
     * @return array All matches in a multi-dimensional array ordered according to flags
426
     */
427
    abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array;
428
429
    /**
430
     * @return static
431
     */
432
    abstract public function padBoth(int $length, string $padStr = ' '): self;
433
434
    /**
435
     * @return static
436
     */
437
    abstract public function padEnd(int $length, string $padStr = ' '): self;
438
439
    /**
440
     * @return static
441
     */
442
    abstract public function padStart(int $length, string $padStr = ' '): self;
443
444
    /**
445
     * @return static
446
     */
447
    abstract public function prepend(string ...$prefix): self;
448
449
    /**
450
     * @return static
451
     */
452
    public function repeat(int $multiplier): self
453
    {
454
        if (0 > $multiplier) {
455
            throw new InvalidArgumentException(sprintf('Multiplier must be positive, %d given.', $multiplier));
456
        }
457
458
        $str = clone $this;
459
        $str->string = str_repeat($str->string, $multiplier);
460
461
        return $str;
462
    }
463
464
    /**
465
     * @return static
466
     */
467
    abstract public function replace(string $from, string $to): self;
468
469
    /**
470
     * @param string|callable $to
471
     *
472
     * @return static
473
     */
474
    abstract public function replaceMatches(string $fromRegexp, $to): self;
475
476
    /**
477
     * @return static
478
     */
479
    abstract public function reverse(): self;
480
481
    /**
482
     * @return static
483
     */
484
    abstract public function slice(int $start = 0, int $length = null): self;
485
486
    /**
487
     * @return static
488
     */
489
    abstract public function snake(): self;
490
491
    /**
492
     * @return static
493
     */
494
    abstract public function splice(string $replacement, int $start = 0, int $length = null): self;
495
496
    /**
497
     * @return static[]
498
     */
499
    public function split(string $delimiter, int $limit = null, int $flags = null): array
500
    {
501
        if (null === $flags) {
502
            throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.');
503
        }
504
505
        if ($this->ignoreCase) {
506
            $delimiter .= 'i';
507
        }
508
509
        set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
510
511
        try {
512
            if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) {
513
                $lastError = preg_last_error();
514
515
                foreach (get_defined_constants(true)['pcre'] as $k => $v) {
516
                    if ($lastError === $v && '_ERROR' === substr($k, -6)) {
517
                        throw new RuntimeException('Splitting failed with '.$k.'.');
518
                    }
519
                }
520
521
                throw new RuntimeException('Splitting failed with unknown error code.');
522
            }
523
        } finally {
524
            restore_error_handler();
525
        }
526
527
        $str = clone $this;
528
529
        if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) {
530
            foreach ($chunks as &$chunk) {
531
                $str->string = $chunk[0];
532
                $chunk[0] = clone $str;
533
            }
534
        } else {
535
            foreach ($chunks as &$chunk) {
536
                $str->string = $chunk;
537
                $chunk = clone $str;
538
            }
539
        }
540
541
        return $chunks;
542
    }
543
544
    /**
545
     * @param string|string[] $prefix
546
     */
547
    public function startsWith($prefix): bool
548
    {
549
        if (!\is_array($prefix) && !$prefix instanceof \Traversable) {
550
            throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class));
551
        }
552
553
        foreach ($prefix as $prefix) {
554
            if ($this->startsWith((string) $prefix)) {
555
                return true;
556
            }
557
        }
558
559
        return false;
560
    }
561
562
    /**
563
     * @return static
564
     */
565
    abstract public function title(bool $allWords = false): self;
566
567
    public function toByteString(string $toEncoding = null): ByteString
568
    {
569
        $b = new ByteString();
570
571
        $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding;
572
573
        if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') {
0 ignored issues
show
Bug introduced by
The property string cannot be accessed from this context as it is declared protected in class Symfony\Component\String\AbstractString.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
574
            $b->string = $this->string;
0 ignored issues
show
Bug introduced by
The property string cannot be accessed from this context as it is declared protected in class Symfony\Component\String\AbstractString.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
575
576
            return $b;
577
        }
578
579
        set_error_handler(static function ($t, $m) { throw new InvalidArgumentException($m); });
580
581
        try {
582
            try {
583
                $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8');
0 ignored issues
show
Bug introduced by
The property string cannot be accessed from this context as it is declared protected in class Symfony\Component\String\AbstractString.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
584
            } catch (InvalidArgumentException $e) {
585
                if (!\function_exists('iconv')) {
586
                    throw $e;
587
                }
588
589
                $b->string = iconv('UTF-8', $toEncoding, $this->string);
0 ignored issues
show
Bug introduced by
The property string cannot be accessed from this context as it is declared protected in class Symfony\Component\String\AbstractString.

This check looks for access to properties that are not accessible from the current context.

If you need to make a property accessible to another context you can either raise its visibility level or provide an accessible getter in the defining class.

Loading history...
590
            }
591
        } finally {
592
            restore_error_handler();
593
        }
594
595
        return $b;
596
    }
597
598
    public function toCodePointString(): CodePointString
599
    {
600
        return new CodePointString($this->string);
601
    }
602
603
    public function toString(): string
604
    {
605
        return $this->string;
606
    }
607
608
    public function toUnicodeString(): UnicodeString
609
    {
610
        return new UnicodeString($this->string);
611
    }
612
613
    /**
614
     * @return static
615
     */
616
    abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;
617
618
    /**
619
     * @return static
620
     */
621
    abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;
622
623
    /**
624
     * @return static
625
     */
626
    abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): self;
627
628
    /**
629
     * @return static
630
     */
631
    public function truncate(int $length, string $ellipsis = '', bool $cut = true): self
632
    {
633
        $stringLength = $this->length();
634
635
        if ($stringLength <= $length) {
636
            return clone $this;
637
        }
638
639
        $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0;
640
641
        if ($length < $ellipsisLength) {
642
            $ellipsisLength = 0;
643
        }
644
645
        if (!$cut) {
646
            if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) {
647
                return clone $this;
648
            }
649
650
            $length += $ellipsisLength;
651
        }
652
653
        $str = $this->slice(0, $length - $ellipsisLength);
654
655
        return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str;
656
    }
657
658
    /**
659
     * @return static
660
     */
661
    abstract public function upper(): self;
662
663
    /**
664
     * Returns the printable length on a terminal.
665
     */
666
    abstract public function width(bool $ignoreAnsiDecoration = true): int;
667
668
    /**
669
     * @return static
670
     */
671
    public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): self
672
    {
673
        $lines = '' !== $break ? $this->split($break) : [clone $this];
674
        $chars = [];
675
        $mask = '';
676
677
        if (1 === \count($lines) && '' === $lines[0]->string) {
678
            return $lines[0];
679
        }
680
681
        foreach ($lines as $i => $line) {
682
            if ($i) {
683
                $chars[] = $break;
684
                $mask .= '#';
685
            }
686
687
            foreach ($line->chunk() as $char) {
688
                $chars[] = $char->string;
689
                $mask .= ' ' === $char->string ? ' ' : '?';
690
            }
691
        }
692
693
        $string = '';
694
        $j = 0;
695
        $b = $i = -1;
696
        $mask = wordwrap($mask, $width, '#', $cut);
697
698
        while (false !== $b = strpos($mask, '#', $b + 1)) {
699
            for (++$i; $i < $b; ++$i) {
700
                $string .= $chars[$j];
701
                unset($chars[$j++]);
702
            }
703
704
            if ($break === $chars[$j] || ' ' === $chars[$j]) {
705
                unset($chars[$j++]);
706
            }
707
708
            $string .= $break;
709
        }
710
711
        $str = clone $this;
712
        $str->string = $string.implode('', $chars);
713
714
        return $str;
715
    }
716
717
    public function __sleep(): array
718
    {
719
        return ['string'];
720
    }
721
722
    public function __clone()
723
    {
724
        $this->ignoreCase = false;
725
    }
726
727
    public function __toString(): string
728
    {
729
        return $this->string;
730
    }
731
}
732