Passed
Pull Request — master (#109)
by
unknown
02:40
created

StringHelper::uppercaseFirstCharacter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Strings;
6
7
use InvalidArgumentException;
8
9
use function array_slice;
10
use function count;
11
use function function_exists;
12
use function max;
13
use function mb_strlen;
14
use function mb_strtolower;
15
use function mb_strtoupper;
16
use function mb_substr;
17
use function str_ends_with;
18
use function str_starts_with;
19
use function strlen;
20
21
/**
22
 * Provides static methods to work with strings.
23
 */
24
final class StringHelper
25
{
26
    /**
27
     * Returns the number of bytes in the given string.
28
     * This method ensures the string is treated as a byte array even if `mbstring.func_overload` is turned on
29
     * by using {@see mb_strlen()}.
30
     *
31
     * @param string|null $input The string being measured for length.
32
     *
33
     * @return int The number of bytes in the given string.
34
     */
35 2
    public static function byteLength(?string $input): int
36
    {
37 2
        return mb_strlen((string)$input, '8bit');
38
    }
39
40
    /**
41
     * Returns the portion of string specified by the start and length parameters.
42
     * This method ensures the string is treated as a byte array by using `mb_substr()`.
43
     *
44
     * @param string $input The input string. Must be one character or longer.
45
     * @param int $start The starting position.
46
     * @param int|null $length The desired portion length. If not specified or `null`, there will be
47
     * no limit on length i.e. the output will be until the end of the string.
48
     *
49
     * @return string The extracted part of string, or FALSE on failure or an empty string.
50
     *
51
     * @see https://www.php.net/manual/en/function.substr.php
52
     */
53 1
    public static function byteSubstring(string $input, int $start, int $length = null): string
54
    {
55 1
        return mb_substr($input, $start, $length ?? mb_strlen($input, '8bit'), '8bit');
56
    }
57
58
    /**
59
     * Returns the trailing name component of a path.
60
     * This method is similar to the php function `basename()` except that it will
61
     * treat both \ and / as directory separators, independent of the operating system.
62
     * This method was mainly created to work on php namespaces. When working with real
63
     * file paths, PHP's `basename()` should work fine for you.
64
     * Note: this method is not aware of the actual filesystem, or path components such as "..".
65
     *
66
     * @param string $path A path string.
67
     * @param string $suffix If the name component ends in suffix this will also be cut off.
68
     *
69
     * @return string The trailing name component of the given path.
70
     *
71
     * @see https://www.php.net/manual/en/function.basename.php
72
     */
73 1
    public static function baseName(string $path, string $suffix = ''): string
74
    {
75 1
        $length = mb_strlen($suffix);
76 1
        if ($length > 0 && mb_substr($path, -$length) === $suffix) {
77 1
            $path = mb_substr($path, 0, -$length);
78
        }
79 1
        $path = rtrim(str_replace('\\', '/', $path), '/\\');
80 1
        $position = mb_strrpos($path, '/');
81 1
        if ($position !== false) {
82 1
            return mb_substr($path, $position + 1);
83
        }
84
85 1
        return $path;
86
    }
87
88
    /**
89
     * Returns parent directory's path.
90
     * This method is similar to `dirname()` except that it will treat
91
     * both \ and / as directory separators, independent of the operating system.
92
     *
93
     * @param string $path A path string.
94
     *
95
     * @return string The parent directory's path.
96
     *
97
     * @see https://www.php.net/manual/en/function.basename.php
98
     */
99 1
    public static function directoryName(string $path): string
100
    {
101 1
        $position = mb_strrpos(str_replace('\\', '/', $path), '/');
102 1
        if ($position !== false) {
103 1
            return mb_substr($path, 0, $position);
104
        }
105
106 1
        return '';
107
    }
108
109
    /**
110
     * Get part of string.
111
     *
112
     * @param string $string To get substring from.
113
     * @param int $start Character to start at.
114
     * @param int|null $length Number of characters to get.
115
     * @param string $encoding The encoding to use, defaults to "UTF-8".
116
     *
117
     * @see https://php.net/manual/en/function.mb-substr.php
118
     *
119
     * @return string
120
     */
121 15
    public static function substring(string $string, int $start, int $length = null, string $encoding = 'UTF-8'): string
122
    {
123 15
        return mb_substr($string, $start, $length, $encoding);
124
    }
125
126
    /**
127
     * Replace text within a portion of a string.
128
     *
129
     * @param string $string The input string.
130
     * @param string $replacement The replacement string.
131
     * @param int $start Position to begin replacing substring at.
132
     * If start is non-negative, the replacing will begin at the start'th offset into string.
133
     * If start is negative, the replacing will begin at the start'th character from the end of string.
134
     * @param int|null $length Length of the substring to be replaced.
135
     * If given and is positive, it represents the length of the portion of string which is to be replaced.
136
     * If it is negative, it represents the number of characters from the end of string at which to stop replacing.
137
     * If it is not given, then it will default to the length of the string; i.e. end the replacing at the end of string.
138
     * If length is zero then this function will have the effect of inserting replacement into string at the given start offset.
139
     * @param string $encoding The encoding to use, defaults to "UTF-8".
140
     *
141
     * @return string
142
     */
143 9
    public static function replaceSubstring(string $string, string $replacement, int $start, ?int $length = null, string $encoding = 'UTF-8'): string
144
    {
145 9
        $stringLength = mb_strlen($string, $encoding);
146
147 9
        if ($start < 0) {
148 2
            $start = max(0, $stringLength + $start);
149 7
        } elseif ($start > $stringLength) {
150 1
            $start = $stringLength;
151
        }
152
153 9
        if ($length !== null && $length < 0) {
154 3
            $length = max(0, $stringLength - $start + $length);
155 6
        } elseif ($length === null || $length > $stringLength) {
156 5
            $length = $stringLength;
157
        }
158
159 9
        if (($start + $length) > $stringLength) {
160 4
            $length = $stringLength - $start;
161
        }
162
163 9
        return mb_substr($string, 0, $start, $encoding) . $replacement . mb_substr($string, $start + $length, $stringLength - $start - $length, $encoding);
164
    }
165
166
    /**
167
     * Check if given string starts with specified substring.
168
     * Binary and multibyte safe.
169
     *
170
     * @param string $input Input string.
171
     * @param string|null $with Part to search inside the $string.
172
     *
173
     * @return bool Returns true if first input starts with second input, false otherwise.
174
     */
175 19
    public static function startsWith(string $input, ?string $with): bool
176
    {
177 19
        if ($with === null) {
178 1
            return true;
179
        }
180
181 18
        if (function_exists('\str_starts_with')) {
182 18
            return str_starts_with($input, $with);
183
        }
184
185
        $bytes = self::byteLength($with);
186
        if ($bytes === 0) {
187
            return true;
188
        }
189
190
        return strncmp($input, $with, $bytes) === 0;
191
    }
192
193
    /**
194
     * Check if given string starts with specified substring ignoring case.
195
     * Binary and multibyte safe.
196
     *
197
     * @param string $input Input string.
198
     * @param string|null $with Part to search inside the $string.
199
     *
200
     * @return bool Returns true if first input starts with second input, false otherwise.
201
     */
202 1
    public static function startsWithIgnoringCase(string $input, ?string $with): bool
203
    {
204 1
        $bytes = self::byteLength($with);
205 1
        if ($bytes === 0) {
206 1
            return true;
207
        }
208
209
        /**
210
         * @psalm-suppress PossiblyNullArgument
211
         */
212 1
        return self::lowercase(self::substring($input, 0, $bytes, '8bit')) === self::lowercase($with);
0 ignored issues
show
Bug introduced by
It seems like $with can also be of type null; however, parameter $string of Yiisoft\Strings\StringHelper::lowercase() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

212
        return self::lowercase(self::substring($input, 0, $bytes, '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
213
    }
214
215
    /**
216
     * Check if given string ends with specified substring.
217
     * Binary and multibyte safe.
218
     *
219
     * @param string $input Input string to check.
220
     * @param string|null $with Part to search inside of the $string.
221
     *
222
     * @return bool Returns true if first input ends with second input, false otherwise.
223
     */
224 19
    public static function endsWith(string $input, ?string $with): bool
225
    {
226 19
        if ($with === null) {
227 1
            return true;
228
        }
229
230 18
        if (function_exists('\str_ends_with')) {
231 18
            return str_ends_with($input, $with);
232
        }
233
234
        $bytes = self::byteLength($with);
235
        if ($bytes === 0) {
236
            return true;
237
        }
238
239
        // Warning check, see https://php.net/manual/en/function.substr-compare.php#refsect1-function.substr-compare-returnvalues
240
        if (self::byteLength($input) < $bytes) {
241
            return false;
242
        }
243
244
        return substr_compare($input, $with, -$bytes, $bytes) === 0;
245
    }
246
247
    /**
248
     * Check if given string ends with specified substring.
249
     * Binary and multibyte safe.
250
     *
251
     * @param string $input Input string to check.
252
     * @param string|null $with Part to search inside of the $string.
253
     *
254
     * @return bool Returns true if first input ends with second input, false otherwise.
255
     */
256 1
    public static function endsWithIgnoringCase(string $input, ?string $with): bool
257
    {
258 1
        $bytes = self::byteLength($with);
259 1
        if ($bytes === 0) {
260 1
            return true;
261
        }
262
263
        /**
264
         * @psalm-suppress PossiblyNullArgument
265
         */
266 1
        return self::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === self::lowercase($with);
0 ignored issues
show
Bug introduced by
It seems like $with can also be of type null; however, parameter $string of Yiisoft\Strings\StringHelper::lowercase() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

266
        return self::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
267
    }
268
269
    /**
270
     * Truncates a string from the beginning to the number of characters specified.
271
     *
272
     * @param string $input String to process.
273
     * @param int $length Maximum length of the truncated string including trim marker.
274
     * @param string $trimMarker String to append to the beginning.
275
     * @param string $encoding The encoding to use, defaults to "UTF-8".
276
     *
277
     * @return string
278
     */
279 1
    public static function truncateBegin(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
280
    {
281 1
        $inputLength = mb_strlen($input, $encoding);
282
283 1
        if ($inputLength <= $length) {
284 1
            return $input;
285
        }
286
287 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
288 1
        return self::replaceSubstring($input, $trimMarker, 0, -$length + $trimMarkerLength, $encoding);
289
    }
290
291
    /**
292
     * Truncates a string in the middle. Keeping start and end.
293
     * `StringHelper::truncateMiddle('Hello world number 2', 8)` produces "Hell…r 2".
294
     *
295
     * @param string $input The string to truncate.
296
     * @param int $length Maximum length of the truncated string including trim marker.
297
     * @param string $trimMarker String to append in the middle of truncated string.
298
     * @param string $encoding The encoding to use, defaults to "UTF-8".
299
     *
300
     * @return string The truncated string.
301
     */
302 2
    public static function truncateMiddle(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
303
    {
304 2
        $inputLength = mb_strlen($input, $encoding);
305
306 2
        if ($inputLength <= $length) {
307 1
            return $input;
308
        }
309
310 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
311 1
        $start = (int)ceil(($length - $trimMarkerLength) / 2);
312 1
        $end = $length - $start - $trimMarkerLength;
313
314 1
        return self::replaceSubstring($input, $trimMarker, $start, -$end, $encoding);
315
    }
316
317
    /**
318
     * Truncates a string from the end to the number of characters specified.
319
     *
320
     * @param string $input The string to truncate.
321
     * @param int $length Maximum length of the truncated string including trim marker.
322
     * @param string $trimMarker String to append to the end of truncated string.
323
     * @param string $encoding The encoding to use, defaults to "UTF-8".
324
     *
325
     * @return string The truncated string.
326
     */
327 1
    public static function truncateEnd(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
328
    {
329 1
        $inputLength = mb_strlen($input, $encoding);
330
331 1
        if ($inputLength <= $length) {
332 1
            return $input;
333
        }
334
335 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
336 1
        return rtrim(mb_substr($input, 0, $length - $trimMarkerLength, $encoding)) . $trimMarker;
337
    }
338
339
    /**
340
     * Truncates a string to the number of words specified.
341
     *
342
     * @param string $input The string to truncate.
343
     * @param int $count How many words from original string to include into truncated string.
344
     * @param string $trimMarker String to append to the end of truncated string.
345
     *
346
     * @return string The truncated string.
347
     */
348 1
    public static function truncateWords(string $input, int $count, string $trimMarker = '…'): string
349
    {
350 1
        $words = preg_split('/(\s+)/u', trim($input), -1, PREG_SPLIT_DELIM_CAPTURE);
351 1
        if (count($words) / 2 > $count) {
352
            /** @var string[] $words */
353 1
            $words = array_slice($words, 0, ($count * 2) - 1);
354 1
            return implode('', $words) . $trimMarker;
355
        }
356
357 1
        return $input;
358
    }
359
360
    /**
361
     * Get string length.
362
     *
363
     * @param string $string String to calculate length for.
364
     * @param string $encoding The encoding to use, defaults to "UTF-8".
365
     *
366
     * @see https://php.net/manual/en/function.mb-strlen.php
367
     *
368
     * @return int
369
     */
370 1
    public static function length(string $string, string $encoding = 'UTF-8'): int
371
    {
372 1
        return mb_strlen($string, $encoding);
373
    }
374
375
    /**
376
     * Counts words in a string.
377
     *
378
     * @param string $input
379
     *
380
     * @return int
381
     */
382 1
    public static function countWords(string $input): int
383
    {
384 1
        return count(preg_split('/\s+/u', $input, -1, PREG_SPLIT_NO_EMPTY));
385
    }
386
387
    /**
388
     * Make a string lowercase.
389
     *
390
     * @param string $string String to process.
391
     * @param string $encoding The encoding to use, defaults to "UTF-8".
392
     *
393
     * @see https://php.net/manual/en/function.mb-strtolower.php
394
     *
395
     * @return string
396
     */
397 3
    public static function lowercase(string $string, string $encoding = 'UTF-8'): string
398
    {
399 3
        return mb_strtolower($string, $encoding);
400
    }
401
402
    /**
403
     * Make a string uppercase.
404
     *
405
     * @param string $string String to process.
406
     * @param string $encoding The encoding to use, defaults to "UTF-8".
407
     *
408
     * @see https://php.net/manual/en/function.mb-strtoupper.php
409
     *
410
     * @return string
411
     */
412 15
    public static function uppercase(string $string, string $encoding = 'UTF-8'): string
413
    {
414 15
        return mb_strtoupper($string, $encoding);
415
    }
416
417
    /**
418
     * Make a string's first character uppercase.
419
     *
420
     * @param string $string The string to be processed.
421
     * @param string $encoding The encoding to use, defaults to "UTF-8".
422
     *
423
     * @return string
424
     *
425
     * @see https://php.net/manual/en/function.ucfirst.php
426
     */
427 14
    public static function uppercaseFirstCharacter(string $string, string $encoding = 'UTF-8'): string
428
    {
429 14
        $firstCharacter = self::substring($string, 0, 1, $encoding);
430 14
        $rest = self::substring($string, 1, null, $encoding);
431
432 14
        return self::uppercase($firstCharacter, $encoding) . $rest;
433
    }
434
435
    /**
436
     * Uppercase the first character of each word in a string.
437
     *
438
     * @param string $string The string to be processed.
439
     * @param string $encoding The encoding to use, defaults to "UTF-8".
440
     *
441
     * @see https://php.net/manual/en/function.ucwords.php
442
     *
443
     * @return string
444
     */
445 10
    public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string
446
    {
447 10
        $words = preg_split('/\s/u', $string, -1, PREG_SPLIT_NO_EMPTY);
448
449 10
        $wordsWithUppercaseFirstCharacter = array_map(static function (string $word) use ($encoding) {
450 9
            return self::uppercaseFirstCharacter($word, $encoding);
451 10
        }, $words);
452
453 10
        return implode(' ', $wordsWithUppercaseFirstCharacter);
454
    }
455
456
    /**
457
     * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
458
     *
459
     * > Note: Base 64 padding `=` may be at the end of the returned string.
460
     * > `=` is not transparent to URL encoding.
461
     *
462
     * @see https://tools.ietf.org/html/rfc4648#page-7
463
     *
464
     * @param string $input The string to encode.
465
     *
466
     * @return string Encoded string.
467
     */
468 4
    public static function base64UrlEncode(string $input): string
469
    {
470 4
        return strtr(base64_encode($input), '+/', '-_');
471
    }
472
473
    /**
474
     * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
475
     *
476
     * @see https://tools.ietf.org/html/rfc4648#page-7
477
     *
478
     * @param string $input Encoded string.
479
     *
480
     * @return string Decoded string.
481
     */
482 4
    public static function base64UrlDecode(string $input): string
483
    {
484 4
        return base64_decode(strtr($input, '-_', '+/'));
485
    }
486
487
    /**
488
     * Split a string to array with non-empty lines.
489
     * Whitespace from the beginning and end of a each line will be stripped.
490
     *
491
     * @param string $string The input string.
492
     * @param string $separator The boundary string. It is a part of regular expression
493
     * so should be taken into account or properly escaped with {@see preg_quote()}.
494
     *
495
     * @return array
496
     */
497 16
    public static function split(string $string, string $separator = '\R'): array
498
    {
499 16
        $string = preg_replace('(^\s*|\s*$)', '', $string);
500 16
        return preg_split('~\s*' . $separator . '\s*~u', $string, -1, PREG_SPLIT_NO_EMPTY);
501
    }
502
503
    /**
504
     * @param string $path The path of where do you want to write a value to `$array`. The path can be described by
505
     * a string when each key should be separated by delimiter. If a path item contains delimiter, it can be escaped
506
     * with "\" (backslash) or a custom delimiter can be used.
507
     * @param string $delimiter A separator, used to parse string key for embedded object property retrieving. Defaults
508
     * to "." (dot).
509
     * @param string $escapeCharacter An escape character, used to escape delimiter. Defaults to "\" (backslash).
510
     * @param bool $preserveDelimiterEscaping Whether to preserve delimiter escaping in the items of final array (in
511
     * case of using string as an input). When `false`, "\" (backslashes) are removed. For a "." as delimiter, "."
512
     * becomes "\.". Defaults to `false`.
513
     *
514
     * @return string[]
515
     *
516
     * @psalm-return list<string>
517
     */
518 34
    public static function parsePath(
519
        string $path,
520
        string $delimiter = '.',
521
        string $escapeCharacter = '\\',
522
        bool $preserveDelimiterEscaping = false
523
    ): array {
524 34
        if (strlen($delimiter) !== 1) {
525 1
            throw new InvalidArgumentException('Only 1 character is allowed for delimiter.');
526
        }
527
528 33
        if (strlen($escapeCharacter) !== 1) {
529 1
            throw new InvalidArgumentException('Only 1 escape character is allowed.');
530
        }
531
532 32
        if ($delimiter === $escapeCharacter) {
533 1
            throw new InvalidArgumentException('Delimiter and escape character must be different.');
534
        }
535
536 31
        if ($path === '') {
537 2
            return [];
538
        }
539
540
        /** @psalm-var non-empty-list<array{0:string, 1:int}> $matches */
541 29
        $matches = preg_split(
542 29
            sprintf(
543 29
                '/(?<!%1$s)((?>%1$s%1$s)*)%2$s/',
544 29
                preg_quote($escapeCharacter, '/'),
545 29
                preg_quote($delimiter, '/')
546 29
            ),
547 29
            $path,
548 29
            -1,
549 29
            PREG_SPLIT_OFFSET_CAPTURE
550 29
        );
551 29
        $result = [];
552 29
        $countResults = count($matches);
553 29
        for ($i = 1; $i < $countResults; $i++) {
554 25
            $l = $matches[$i][1] - $matches[$i - 1][1] - strlen($matches[$i - 1][0]) - 1;
555 25
            $result[] = $matches[$i - 1][0] . ($l > 0 ? str_repeat($escapeCharacter, $l) : '');
556
        }
557 29
        $result[] = $matches[$countResults - 1][0];
558
559 29
        if ($preserveDelimiterEscaping === true) {
560 1
            return $result;
561
        }
562
563 28
        return array_map(
564 28
            static function (string $key) use ($delimiter, $escapeCharacter): string {
565 28
                return str_replace(
566 28
                    [
567 28
                        $escapeCharacter . $escapeCharacter,
568 28
                        $escapeCharacter . $delimiter,
569 28
                    ],
570 28
                    [
571 28
                        $escapeCharacter,
572 28
                        $delimiter,
573 28
                    ],
574 28
                    $key
575 28
                );
576 28
            },
577 28
            $result
578 28
        );
579
    }
580
581
    /**
582
     * Strip Unicode whitespace (with property White_Space=yes) or other characters from the beginning and end of a string.
583
     * Input string and pattern are treated as UTF-8.
584
     *
585
     * @see https://en.wikipedia.org/wiki/Whitespace_character#Unicode
586
     * @see https://www.php.net/manual/function.preg-replace
587
     *
588
     * @param string $string The input string.
589
     * @param string $pattern PCRE regex pattern to search for, as a string. Quote $pattern if it contains special regular expression characters.
590
     * @see https://www.php.net/manual/function.preg-quote.php
591
     *
592
     * @return string
593
     */
594 8
    public static function trim(string $string, string $pattern = "\pC\pZ"): string
595
    {
596 8
        if (!preg_match('##u', $pattern)) {
597 1
            throw new InvalidArgumentException('Pattern must be valid UTF-8 string.');
598
        }
599
600 7
        return preg_replace("#^[$pattern]+|[$pattern]+$#uD", '', $string);
601
    }
602
603
    /**
604
     * Strip Unicode whitespace (with property White_Space=yes) or other characters from the beginning of a string
605
     *
606
     * @see self::trim()
607
     */
608 7
    public static function ltrim(string $string, string $pattern = "\pC\pZ"): string
609
    {
610 7
        if (!preg_match('##u', $pattern)) {
611 1
            throw new InvalidArgumentException('Pattern must be valid UTF-8 string.');
612
        }
613
614 6
        return preg_replace("#^[$pattern]+#u", '', $string);
615
    }
616
617
    /**
618
     * Strip Unicode whitespace (with property White_Space=yes) or other characters from the end of a string
619
     *
620
     * @see self::trim()
621
     */
622 7
    public static function rtrim(string $string, string $pattern = "\pC\pZ"): string
623
    {
624 7
        if (!preg_match('##u', $pattern)) {
625 1
            throw new InvalidArgumentException('Pattern must be valid UTF-8 string.');
626
        }
627
628 6
        return preg_replace("#[$pattern]+$#uD", '', $string);
629
    }
630
}
631