Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.96% covered (success)
93.96%
140 / 149
74.19% covered (warning)
74.19%
23 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
Text
93.96% covered (success)
93.96%
140 / 149
74.19% covered (warning)
74.19%
23 / 31
80.38
0.00% covered (danger)
0.00%
0 / 1
 isNullOrEmpty
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isNullOrWhiteSpace
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 requireNotNullOrEmpty
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 requireNotNullOrWhiteSpace
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getLength
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getByteCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromCodePointCore
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 fromCodePoint
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 replaceMap
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
3.01
 format
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatNumber
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPosition
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getLastPosition
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 startsWith
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
6.04
 endsWith
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
6.04
 contains
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
6.05
 substring
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 toLower
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toUpper
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 split
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 splitLines
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 join
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createTrimPattern
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 trim
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 trimStart
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 trimEnd
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 dump
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 replace
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
5.20
 repeat
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 toCharacters
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 toString
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core;
6
7use Throwable;
8use PeServer\Core\Throws\ArgumentException;
9use PeServer\Core\Throws\Throws;
10use PeServer\Core\Throws\RegexException;
11use PeServer\Core\Throws\StringException;
12
13/**
14 * 文字列操作。
15 */
16abstract class Text
17{
18    #region define
19
20    /** トリム対象文字一覧。 */
21    public const TRIM_CHARACTERS = " \n\r\t\v\0 ";
22    /** PHP の使ってる対象文字一覧 */
23    public const TRIM_PHP_CHARACTERS = " \t\n\r\0\x0B";
24    private const TRIM_TARGET_START = -1;
25    private const TRIM_TARGET_END = 1;
26    private const TRIM_TARGET_BOTH = 0;
27
28    /** 空文字列。 */
29    public const EMPTY = '';
30
31    #endregion
32
33    #region function
34
35    /**
36     * 文字列が `null` か空か
37     *
38     * @param string|null $s 対象文字列。
39     * @return bool 真: `null`か空。
40     * @phpstan-assert-if-false non-empty-string $s
41     */
42    public static function isNullOrEmpty(?string $s): bool
43    {
44        if ($s === null) {
45            return true;
46        }
47
48        if ($s === '0') {
49            return false;
50        }
51
52        return empty($s);
53    }
54
55    /**
56     * 文字列がnullかホワイトスペースのみで構築されているか
57     *
58     * `TRIM_CHARACTERS` がホワイトスペースとして扱われる。
59     *
60     * @param string|null $s 対象文字列。
61     * @return bool 真: nullかホワイトスペースのみ。
62     * @phpstan-assert-if-false non-empty-string $s
63     */
64    public static function isNullOrWhiteSpace(?string $s): bool
65    {
66        if (self::isNullOrEmpty($s)) {
67            return true;
68        }
69
70        return strlen(self::trim($s)) === 0;
71    }
72
73    /**
74     * 文字列が `null` か空の場合に代替文字列を返す。
75     *
76     * @param string|null $s
77     * @param string $fallback
78     * @return string
79     */
80    public static function requireNotNullOrEmpty(?string $s, string $fallback): string
81    {
82        if (self::isNullOrEmpty($s)) {
83            return $fallback;
84        }
85
86        return $s;
87    }
88
89    /**
90     * 文字列がnullかホワイトスペースのみで構築されている場合に代替文字列を返す。
91     *
92     * @param string|null $s
93     * @param string $fallback
94     * @return string
95     */
96    public static function requireNotNullOrWhiteSpace(?string $s, string $fallback): string
97    {
98        if (self::isNullOrWhiteSpace($s)) {
99            return $fallback;
100        }
101
102        return $s;
103    }
104
105    /**
106     * 文字列長を取得。
107     *
108     * `mb_strlen` ラッパー。
109     *
110     * @param string $value 対象文字列。
111     * @return int 文字数。
112     * @phpstan-return non-negative-int
113     * @see https://www.php.net/manual/function.mb-strlen.php
114     */
115    public static function getLength(string $value): int
116    {
117        return mb_strlen($value);
118    }
119
120    /*
121    public static function getCharacterLength(string $value): int
122    {
123        $length = self::getLength($value);
124        if($length < 2) {
125            return $length;
126        }
127        return \grapheme_strlen($value);
128    }
129    */
130
131    /**
132     * 文字列バイト数を取得。
133     *
134     * `strlen` ラッパー。
135     *
136     * @param string $value 対象文字列。
137     * @return int バイト数。
138     * @phpstan-return non-negative-int
139     * @see https://www.php.net/manual/function.strlen.php
140     */
141    public static function getByteCount(string $value): int
142    {
143        return strlen($value);
144    }
145
146    private static function fromCodePointCore(int $value): string
147    {
148        $single = mb_chr($value);
149        if ($single === false) { //@phpstan-ignore-line [PHP_VERSION]
150            throw new ArgumentException();
151        }
152
153        return $single;
154    }
155
156    /**
157     * Unicode のコードポイントに対応する文字を返す。
158     *
159     * `mb_chr` ラッパー。
160     *
161     * @param int|int[] $value
162     * @phpstan-param non-negative-int|non-negative-int[] $value
163     * @return string
164     * @see https://www.php.net/manual/function.mb-chr.php
165     * @throws ArgumentException
166     */
167    public static function fromCodePoint(int|array $value): string
168    {
169        if (is_int($value)) {
170            return self::fromCodePointCore($value);
171        }
172
173        $result = '';
174        foreach ($value as $cp) {
175            if (!is_int($cp)) { //@phpstan-ignore-line [DOCTYPE]
176                throw new ArgumentException();
177            }
178
179            $result .= self::fromCodePointCore($cp);
180        }
181
182        return $result;
183    }
184
185    /**
186     * プレースホルダー文字列置き換え処理
187     *
188     * @param string $source 元文字列
189     * @phpstan-param literal-string $source
190     * @param array<string,string> $map 置き換え対象辞書
191     * @param non-empty-string $head プレースホルダー先頭
192     * @param non-empty-string $tail プレースホルダー終端
193     * @return string 置き換え後文字列
194     * @throws StringException なんかもうあかんかった
195     */
196    public static function replaceMap(string $source, array $map, string $head = '{', string $tail = '}'): string
197    {
198        Throws::throwIfNullOrEmpty($head, Text::EMPTY, StringException::class);
199        Throws::throwIfNullOrEmpty($tail, Text::EMPTY, StringException::class);
200
201        $regex = new Regex();
202        $escHead = $regex->escape($head);
203        $escTail = $regex->escape($tail);
204        $pattern = "/$escHead(.+?)$escTail/";
205
206        try {
207            $result = $regex->replaceCallback(
208                $source,
209                $pattern,
210                function ($matches) use ($map) {
211                    if (isset($map[$matches[1]])) {
212                        return $map[$matches[1]];
213                    }
214                    return Text::EMPTY;
215                }
216            );
217
218            return $result;
219        } catch (Throwable $ex) {
220            Throws::reThrow(StringException::class, $ex);
221        }
222    }
223
224    /**
225     * `sprintf` ラッパー。
226     *
227     * 単純な文字列置き換えであれば `Text::replaceMap` を使用する。
228     *
229     * @param string $format
230     * @param mixed ...$values
231     * @phpstan-param FormatAlias ...$values
232     * @return string
233     * @see https://www.php.net/manual/function.sprintf.php
234     * @see Text::replaceMap()
235     */
236    public static function format(string $format, string|int|float ...$values): string
237    {
238        return sprintf($format, ...$values);
239    }
240
241    /**
242     * 数字を千の位毎にグループ化してフォーマット
243     *
244     * @param int|float $number フォーマットする数値
245     * @param int $decimals 小数点以下の桁数。 0 を指定すると、 戻り値の $decimalSeparator は省略されます
246     * @param string|null $decimalSeparator 小数点を表す区切り文字
247     * @param string|null $thousandsSeparator 千の位毎の区切り文字
248     * @return string 置き換え後文字列
249     * @see https://www.php.net/manual/function.number-format.php
250     */
251    public static function formatNumber(int|float $number, int $decimals = 0, ?string $decimalSeparator = '.', ?string $thousandsSeparator = ','): string
252    {
253        return number_format($number, $decimals, $decimalSeparator, $thousandsSeparator);
254    }
255
256    /**
257     * 文字列位置を取得。
258     *
259     * @param string $haystack 対象文字列。
260     * @param string $needle 検索文字列。
261     * @param int $offset 開始文字数目。
262     * @phpstan-param non-negative-int $offset
263     * @return int 見つかった文字位置。見つかんない場合は `-1`
264     * @phpstan-return non-negative-int|-1
265     * @throws ArgumentException
266     */
267    public static function getPosition(string $haystack, string $needle, int $offset = 0): int
268    {
269        if ($offset < 0) { //@phpstan-ignore-line non-negative-int
270            throw new ArgumentException('$offset');
271        }
272
273        $result = mb_strpos($haystack, $needle, $offset);
274        if ($result === false) {
275            return -1;
276        }
277
278        return $result;
279    }
280
281    /**
282     * 文字列位置を取得。
283     *
284     * @param string $haystack 対象文字列。
285     * @param string $needle 検索文字列。
286     * @param int $offset 終端文字数目。
287     * @phpstan-param non-negative-int $offset
288     * @return int 見つかった文字位置。見つかんない場合は `-1`
289     * @phpstan-return non-negative-int|-1
290     * @throws ArgumentException
291     */
292    public static function getLastPosition(string $haystack, string $needle, int $offset = 0): int
293    {
294        if ($offset < 0) { //@phpstan-ignore-line [DOCTYPE]
295            throw new ArgumentException('$offset');
296        }
297
298        $result = mb_strrpos($haystack, $needle, $offset);
299        if ($result === false) {
300            return -1;
301        }
302
303        return $result;
304    }
305
306    /**
307     * 先頭文字列一致判定。
308     *
309     * @param string $haystack 対象文字列。
310     * @param string $needle 検索文字列。
311     * @param boolean $ignoreCase 大文字小文字を無視するか。
312     * @return boolean
313     */
314    public static function startsWith(string $haystack, string $needle, bool $ignoreCase): bool
315    {
316        if (!$ignoreCase && function_exists('str_starts_with')) {
317            return str_starts_with($haystack, $needle);
318        }
319
320        if (self::isNullOrEmpty($needle)) {
321            return true;
322        }
323        if (strlen($haystack) < strlen($needle)) {
324            return false;
325        }
326
327        $word = mb_substr($haystack, 0, mb_strlen($needle));
328
329        if ($ignoreCase) {
330            return !strcasecmp($needle, $word);
331        }
332        return $needle === $word;
333    }
334
335    /**
336     * 終端文字列一致判定。
337     *
338     * @param string $haystack 対象文字列。
339     * @param string $needle 検索文字列。
340     * @param boolean $ignoreCase 大文字小文字を無視するか。
341     * @return boolean
342     */
343    public static function endsWith(string $haystack, string $needle, bool $ignoreCase): bool
344    {
345        if (!$ignoreCase && function_exists('str_ends_with')) {
346            return str_ends_with($haystack, $needle);
347        }
348
349        if (self::isNullOrEmpty($needle)) {
350            return true;
351        }
352        if (strlen($haystack) < strlen($needle)) {
353            return false;
354        }
355
356        $word = mb_substr($haystack, -mb_strlen($needle));
357
358        if ($ignoreCase) {
359            return !strcasecmp($needle, $word);
360        }
361        return $needle === $word;
362    }
363
364    /**
365     * 文字列を含んでいるか判定。
366     *
367     * @param string $haystack 対象文字列。
368     * @param string $needle 検索文字列。
369     * @param boolean $ignoreCase 大文字小文字を無視するか。
370     * @return boolean
371     */
372    public static function contains(string $haystack, string $needle, bool $ignoreCase): bool
373    {
374        if (!$ignoreCase && function_exists('str_contains')) {
375            return str_contains($haystack, $needle);
376        }
377
378        if (self::isNullOrEmpty($needle)) {
379            return true;
380        }
381        if (strlen($haystack) < strlen($needle)) {
382            return false;
383        }
384
385        if ($ignoreCase) {
386            return stripos($haystack, $needle) !== false;
387        }
388
389        return strpos($haystack, $needle) !== false;
390    }
391
392    /**
393     * 文字列部分切り出し。
394     *
395     * @param string $value 対象文字列。
396     * @param int $offset 開始文字数目。負数の場合は後ろから。
397     * @param int $length 抜き出す長さ。負数の場合は最後まで($offset)
398     * @return string 切り抜き後文字列。
399     */
400    public static function substring(string $value, int $offset, int $length = -1): string
401    {
402        return mb_substr($value, $offset, 0 <= $length ? $length : null);
403    }
404
405    /**
406     * 大文字を小文字に変換。
407     *
408     * @param string $value
409     * @return string
410     */
411    public static function toLower(string $value): string
412    {
413        return mb_strtolower($value);
414    }
415
416    /**
417     * 小文字を大文字に変換。
418     *
419     * @param string $value
420     * @return string
421     */
422    public static function toUpper(string $value): string
423    {
424        return mb_strtoupper($value);
425    }
426
427    /**
428     * 文字列分割。
429     *
430     * `explode` ラッパー。
431     *
432     * @param string $value 対象文字列。
433     * @param non-empty-string $separator 分割対象文字列。
434     * @param int $limit 分割数。
435     * @return string[] 分割された文字列。
436     * @throws ArgumentException 分割失敗(PHP8未満)
437     * @throws \ValueError 分割失敗(PHP8以上)
438     * @see https://www.php.net/manual/function.explode.php
439     */
440    public static function split(string $value, string $separator, int $limit = PHP_INT_MAX): array
441    {
442        if (Text::isNullOrEmpty($separator)) { //@phpstan-ignore-line [DOCTYPE]
443            throw new ArgumentException();
444        }
445
446        $result = explode($separator, $value, $limit);
447
448        return $result;
449    }
450
451    /**
452     * 文字列を改行で分割。
453     *
454     * @param string $value
455     * @return string[]
456     */
457    public static function splitLines(string $value): array
458    {
459        $lfValue = self::replace($value, ["\r\n", "\r"], "\n");
460        return self::split($lfValue, "\n");
461    }
462
463    /**
464     * 文字列結合。
465     *
466     * `implode` ラッパー。
467     *
468     * @param string $separator
469     * @param string[] $values
470     * @return string
471     * @see https://www.php.net/manual/function.implode.php
472     */
473    public static function join(string $separator, array $values): string
474    {
475        return implode($separator, $values);
476    }
477
478    /**
479     * トリム用パターンの生成。
480     *
481     * @param Regex $regex
482     * @param string $characters
483     * @param int $trimTarget
484     * @phpstan-param self::TRIM_TARGET_* $trimTarget
485     * @return string
486     */
487    private static function createTrimPattern(Regex $regex, string $characters, int $trimTarget): string
488    {
489        $escapedCharactersPattern = $regex->escape($characters);
490        $rangePattern = self::replace($escapedCharactersPattern, '\.\.', '-');
491
492        return match ($trimTarget) {
493            self::TRIM_TARGET_START => "/\A[$rangePattern]++|/",
494            self::TRIM_TARGET_END => "/[$rangePattern]++\z/",
495            self::TRIM_TARGET_BOTH => "/\A[$rangePattern]++|[$rangePattern]++\z/",
496        };
497    }
498
499    /**
500     * トリム処理。
501     *
502     * `trim` ラッパー。
503     *
504     * @param string $value 対象文字列。
505     * @param string $characters トリム対象文字。
506     * @return string トリム後文字列。
507     * @see https://www.php.net/manual/function.trim.php
508     */
509    public static function trim(string $value, string $characters = self::TRIM_CHARACTERS): string
510    {
511        // 1byte文字構成であればそのまま
512        if (strlen($characters) == self::getLength($characters)) {
513            return \trim($value, $characters);
514        }
515
516        $regex = new Regex();
517        $pattern = self::createTrimPattern($regex, $characters, self::TRIM_TARGET_BOTH);
518        $result = $regex->replace($value, $pattern, '');
519
520        return $result;
521    }
522
523    /**
524     * 先頭トリム。
525     *
526     * `ltrim` ラッパー。
527     *
528     * @param string $value 対象文字列。
529     * @param string $characters トリム対象文字。
530     * @return string トリム後文字列。
531     * @see https://www.php.net/manual/function.ltrim.php
532     */
533    public static function trimStart(string $value, string $characters = self::TRIM_CHARACTERS): string
534    {
535        // 1byte文字構成であればそのまま
536        if (strlen($characters) == self::getLength($characters)) {
537            return ltrim($value, $characters);
538        }
539
540        $regex = new Regex();
541        $pattern = self::createTrimPattern($regex, $characters, self::TRIM_TARGET_START);
542        $result = $regex->replace($value, $pattern, '');
543
544        return $result;
545    }
546
547    /**
548     * 終端トリム。
549     *
550     * `rtrim` ラッパー。
551     *
552     * @param string $value 対象文字列。
553     * @param string $characters トリム対象文字。
554     * @return string トリム後文字列。
555     * @see https://www.php.net/manual/function.rtrim.php
556     */
557    public static function trimEnd(string $value, string $characters = self::TRIM_CHARACTERS): string
558    {
559        // 1byte文字構成であればそのまま
560        if (strlen($characters) == self::getLength($characters)) {
561            return rtrim($value, $characters);
562        }
563
564        $regex = new Regex();
565        $pattern = self::createTrimPattern($regex, $characters, self::TRIM_TARGET_END);
566        $result = $regex->replace($value, $pattern, '');
567
568        return $result;
569    }
570
571
572    /**
573     * データ出力。
574     *
575     * var_export/print_r で迷ったり $return = true 忘れのためのラッパー。
576     * 色々あったけど `var_dump` に落ち着いた感。
577     *
578     * @param mixed $value
579     * @return string
580     */
581    public static function dump($value): string
582    {
583        //return print_r($value, true);
584
585        $val = OutputBuffer::get(fn () => var_dump($value));
586        if ($val->hasNull()) {
587            return $val->toBase64();
588        }
589
590        return $val->toString();
591    }
592
593    /**
594     * 文字列置き換え
595     *
596     * `str_replace` ラッパー。
597     *
598     * @param string $source 入力文字列。
599     * @param string|string[] $oldValue 元文字列(か、元文字列配列)
600     * @param string|string[] $newValue 置き換え文字列(か、元文字列配列)。
601     * @return string 置き換え後文字列。
602     * @see https://www.php.net/manual/function.str-replace.php
603     */
604    public static function replace(string $source, string|array $oldValue, string|array $newValue): string
605    {
606        if (is_string($oldValue) && is_array($newValue)) {
607            throw new ArgumentException('$newValue');
608        }
609
610        if (is_string($oldValue) && $oldValue === $newValue) {
611            return $source;
612        }
613
614        return str_replace($oldValue, $newValue, $source);
615    }
616
617    /**
618     * 文字列を反復。
619     *
620     * str_repeat ラッパー。
621     *
622     * @param string $value
623     * @param int $count
624     * @phpstan-param non-negative-int $count
625     * @return string
626     * @throws ArgumentException 負数。
627     * @see https://www.php.net/manual/function.str-repeat.php
628     */
629    public static function repeat(string $value, int $count): string
630    {
631        //@phpstan-ignore-next-line [DOCTYPE]
632        if ($count < 0) {
633            throw new ArgumentException();
634        }
635
636        return str_repeat($value, $count);
637    }
638
639    /**
640     * 文字列を文字の配列に変換。
641     *
642     * @param non-empty-string $value
643     * @return string[]
644     */
645    public static function toCharacters(string $value): array
646    {
647        if (Text::isNullOrEmpty($value)) { //@phpstan-ignore-line [DOCTYPE]
648            throw new ArgumentException('$value = ' . $value);
649        }
650
651        $length = Text::getLength($value);
652        $charactersArray = [];
653        for ($i = 0; $i < $length; $i++) {
654            $c = self::substring($value, $i, 1);
655            $charactersArray[] = $c;
656        }
657
658        return $charactersArray;
659    }
660
661    /**
662     * 何かしらを文字列変換
663     *
664     * @param mixed $raw
665     * @param string $newline 配列の場合の改行(未指定時に `PHP_EOL`)
666     * @return string
667     */
668    public static function toString(mixed $raw, string $newline = PHP_EOL): string
669    {
670        if (is_string($raw)) {
671            return $raw;
672        }
673
674        if (is_array($raw)) {
675            return self::join($newline, $raw);
676        }
677
678        return strval($raw);
679    }
680
681    #endregion
682}