Passed
Push — master ( 3d00c9...7a9fdc )
by Alexander
02:06
created

StringHelper::replaceSubstring()   B

Complexity

Conditions 8
Paths 18

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 8

Importance

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

208
        return self::lowercase(self::substring($input, 0, $bytes, '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
209
    }
210
211
    /**
212
     * Check if given string ends with specified substring.
213
     * Binary and multibyte safe.
214
     *
215
     * @param string $input Input string to check.
216
     * @param string|null $with Part to search inside of the $string.
217
     *
218
     * @return bool Returns true if first input ends with second input, false otherwise.
219
     */
220 19
    public static function endsWith(string $input, ?string $with): bool
221
    {
222 19
        if ($with === null) {
223 1
            return true;
224
        }
225
226 18
        if (function_exists('\str_ends_with')) {
227 18
            return str_ends_with($input, $with);
228
        }
229
230
        $bytes = self::byteLength($with);
231
        if ($bytes === 0) {
232
            return true;
233
        }
234
235
        // Warning check, see http://php.net/manual/en/function.substr-compare.php#refsect1-function.substr-compare-returnvalues
236
        if (self::byteLength($input) < $bytes) {
237
            return false;
238
        }
239
240
        return substr_compare($input, $with, -$bytes, $bytes) === 0;
241
    }
242
243
    /**
244
     * Check if given string ends with specified substring.
245
     * Binary and multibyte safe.
246
     *
247
     * @param string $input Input string to check.
248
     * @param string|null $with Part to search inside of the $string.
249
     *
250
     * @return bool Returns true if first input ends with second input, false otherwise.
251
     */
252 1
    public static function endsWithIgnoringCase(string $input, ?string $with): bool
253
    {
254 1
        $bytes = self::byteLength($with);
255 1
        if ($bytes === 0) {
256 1
            return true;
257
        }
258
259
        /**
260
         * @psalm-suppress PossiblyNullArgument
261
         */
262 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

262
        return self::lowercase(mb_substr($input, -$bytes, mb_strlen($input, '8bit'), '8bit')) === self::lowercase(/** @scrutinizer ignore-type */ $with);
Loading history...
263
    }
264
265
    /**
266
     * Truncates a string from the beginning to the number of characters specified.
267
     *
268
     * @param string $input String to process.
269
     * @param int $length Maximum length of the truncated string including trim marker.
270
     * @param string $trimMarker String to append to the beginning.
271
     * @param string $encoding The encoding to use, defaults to "UTF-8".
272
     *
273
     * @return string
274
     */
275 1
    public static function truncateBegin(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
276
    {
277 1
        $inputLength = mb_strlen($input, $encoding);
278
279 1
        if ($inputLength <= $length) {
280 1
            return $input;
281
        }
282
283 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
284 1
        return self::replaceSubstring($input, $trimMarker, 0, -$length + $trimMarkerLength, $encoding);
285
    }
286
287
    /**
288
     * Truncates a string in the middle. Keeping start and end.
289
     * `StringHelper::truncateMiddle('Hello world number 2', 8)` produces "Hell…r 2".
290
     *
291
     * @param string $input The string to truncate.
292
     * @param int $length Maximum length of the truncated string including trim marker.
293
     * @param string $trimMarker String to append in the middle of truncated string.
294
     * @param string $encoding The encoding to use, defaults to "UTF-8".
295
     *
296
     * @return string The truncated string.
297
     */
298 2
    public static function truncateMiddle(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
299
    {
300 2
        $inputLength = mb_strlen($input, $encoding);
301
302 2
        if ($inputLength <= $length) {
303 1
            return $input;
304
        }
305
306 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
307 1
        $start = (int)ceil(($length - $trimMarkerLength) / 2);
308 1
        $end = $length - $start - $trimMarkerLength;
309
310 1
        return self::replaceSubstring($input, $trimMarker, $start, -$end, $encoding);
311
    }
312
313
    /**
314
     * Truncates a string from the end to the number of characters specified.
315
     *
316
     * @param string $input The string to truncate.
317
     * @param int $length Maximum length of the truncated string including trim marker.
318
     * @param string $trimMarker String to append to the end of truncated string.
319
     * @param string $encoding The encoding to use, defaults to "UTF-8".
320
     *
321
     * @return string The truncated string.
322
     */
323 1
    public static function truncateEnd(string $input, int $length, string $trimMarker = '…', string $encoding = 'UTF-8'): string
324
    {
325 1
        $inputLength = mb_strlen($input, $encoding);
326
327 1
        if ($inputLength <= $length) {
328 1
            return $input;
329
        }
330
331 1
        $trimMarkerLength = mb_strlen($trimMarker, $encoding);
332 1
        return rtrim(mb_substr($input, 0, $length - $trimMarkerLength, $encoding)) . $trimMarker;
333
    }
334
335
    /**
336
     * Truncates a string to the number of words specified.
337
     *
338
     * @param string $input The string to truncate.
339
     * @param int $count How many words from original string to include into truncated string.
340
     * @param string $trimMarker String to append to the end of truncated string.
341
     *
342
     * @return string The truncated string.
343
     */
344 1
    public static function truncateWords(string $input, int $count, string $trimMarker = '…'): string
345
    {
346 1
        $words = preg_split('/(\s+)/u', trim($input), -1, PREG_SPLIT_DELIM_CAPTURE);
347 1
        if (count($words) / 2 > $count) {
348 1
            return implode('', array_slice($words, 0, ($count * 2) - 1)) . $trimMarker;
349
        }
350
351 1
        return $input;
352
    }
353
354
    /**
355
     * Get string length.
356
     *
357
     * @param string $string String to calculate length for.
358
     * @param string $encoding The encoding to use, defaults to "UTF-8".
359
     *
360
     * @see https://php.net/manual/en/function.mb-strlen.php
361
     *
362
     * @return int
363
     */
364 1
    public static function length(string $string, string $encoding = 'UTF-8'): int
365
    {
366 1
        return mb_strlen($string, $encoding);
367
    }
368
369
    /**
370
     * Counts words in a string.
371
     *
372
     * @param string $input
373
     *
374
     * @return int
375
     */
376 1
    public static function countWords(string $input): int
377
    {
378 1
        return count(preg_split('/\s+/u', $input, -1, PREG_SPLIT_NO_EMPTY));
379
    }
380
381
    /**
382
     * Make a string lowercase.
383
     *
384
     * @param string $string String to process.
385
     * @param string $encoding The encoding to use, defaults to "UTF-8".
386
     *
387
     * @see https://php.net/manual/en/function.mb-strtolower.php
388
     *
389
     * @return string
390
     */
391 3
    public static function lowercase(string $string, string $encoding = 'UTF-8'): string
392
    {
393 3
        return mb_strtolower($string, $encoding);
394
    }
395
396
    /**
397
     * Make a string uppercase.
398
     *
399
     * @param string $string String to process.
400
     * @param string $encoding The encoding to use, defaults to "UTF-8".
401
     *
402
     * @see https://php.net/manual/en/function.mb-strtoupper.php
403
     *
404
     * @return string
405
     */
406 15
    public static function uppercase(string $string, string $encoding = 'UTF-8'): string
407
    {
408 15
        return mb_strtoupper($string, $encoding);
409
    }
410
411
    /**
412
     * Make a string's first character uppercase.
413
     *
414
     * @param string $string The string to be processed.
415
     * @param string $encoding The encoding to use, defaults to "UTF-8".
416
     *
417
     * @return string
418
     *
419
     * @see https://php.net/manual/en/function.ucfirst.php
420
     */
421 14
    public static function uppercaseFirstCharacter(string $string, string $encoding = 'UTF-8'): string
422
    {
423 14
        $firstCharacter = self::substring($string, 0, 1, $encoding);
424 14
        $rest = self::substring($string, 1, null, $encoding);
425
426 14
        return self::uppercase($firstCharacter, $encoding) . $rest;
427
    }
428
429
    /**
430
     * Uppercase the first character of each word in a string.
431
     *
432
     * @param string $string The string to be processed.
433
     * @param string $encoding The encoding to use, defaults to "UTF-8".
434
     *
435
     * @see https://php.net/manual/en/function.ucwords.php
436
     *
437
     * @return string
438
     */
439 10
    public static function uppercaseFirstCharacterInEachWord(string $string, string $encoding = 'UTF-8'): string
440
    {
441 10
        $words = preg_split('/\s/u', $string, -1, PREG_SPLIT_NO_EMPTY);
442
443 10
        $wordsWithUppercaseFirstCharacter = array_map(static function (string $word) use ($encoding) {
444 9
            return self::uppercaseFirstCharacter($word, $encoding);
445 10
        }, $words);
446
447 10
        return implode(' ', $wordsWithUppercaseFirstCharacter);
448
    }
449
450
    /**
451
     * Encodes string into "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
452
     *
453
     * > Note: Base 64 padding `=` may be at the end of the returned string.
454
     * > `=` is not transparent to URL encoding.
455
     *
456
     * @see https://tools.ietf.org/html/rfc4648#page-7
457
     *
458
     * @param string $input The string to encode.
459
     *
460
     * @return string Encoded string.
461
     */
462 4
    public static function base64UrlEncode(string $input): string
463
    {
464 4
        return strtr(base64_encode($input), '+/', '-_');
465
    }
466
467
    /**
468
     * Decodes "Base 64 Encoding with URL and Filename Safe Alphabet" (RFC 4648).
469
     *
470
     * @see https://tools.ietf.org/html/rfc4648#page-7
471
     *
472
     * @param string $input Encoded string.
473
     *
474
     * @return string Decoded string.
475
     */
476 4
    public static function base64UrlDecode(string $input): string
477
    {
478 4
        return base64_decode(strtr($input, '-_', '+/'));
479
    }
480
481
    /**
482
     * Split a string to array with non-empty lines.
483
     * Whitespace from the beginning and end of a each line will be stripped.
484
     *
485
     * @param string $string The input string.
486
     * @param string $separator The boundary string. It is a part of regular expression
487
     * so should be taken into account or properly escaped with {@see preg_quote()}.
488
     *
489
     * @return array
490
     */
491 15
    public static function split(string $string, string $separator = '\R'): array
492
    {
493 15
        $string = preg_replace('(^\s*|\s*$)', '', $string);
494 15
        return preg_split('~\s*' . $separator . '\s*~', $string, -1, PREG_SPLIT_NO_EMPTY);
495
    }
496
}
497