Skip to content

Commit ccb1e65

Browse files
authored
feat(support): add StringHelper
1 parent 1efd3b8 commit ccb1e65

File tree

3 files changed

+372
-0
lines changed

3 files changed

+372
-0
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Support;
6+
7+
use Countable;
8+
9+
final readonly class StringHelper
10+
{
11+
public static function title(string $value): string
12+
{
13+
return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8');
14+
}
15+
16+
public static function lower(string $value): string
17+
{
18+
return mb_strtolower($value, 'UTF-8');
19+
}
20+
21+
public static function upper(string $value): string
22+
{
23+
return mb_strtoupper($value, 'UTF-8');
24+
}
25+
26+
public static function snake(string $value, string $delimiter = '_'): string
27+
{
28+
if (ctype_lower($value)) {
29+
return $value;
30+
}
31+
32+
$value = preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value);
33+
$value = preg_replace('![^'.preg_quote($delimiter).'\pL\pN\s]+!u', $delimiter, static::lower($value));
34+
$value = preg_replace('/\s+/u', $delimiter, $value);
35+
$value = trim($value, $delimiter);
36+
37+
return static::deduplicate($value, $delimiter);
38+
}
39+
40+
public static function kebab(string $value): string
41+
{
42+
return static::snake($value, '-');
43+
}
44+
45+
public static function pascal(string $value): string
46+
{
47+
$words = explode(' ', str_replace(['-', '_'], ' ', $value));
48+
// TODO: use `mb_ucfirst` when it has landed in PHP 8.4
49+
$studlyWords = array_map(static fn (string $word) => ucfirst($word), $words);
50+
51+
return implode('', $studlyWords);
52+
}
53+
54+
public static function deduplicate(string $string, string|array $characters = ' '): string
55+
{
56+
foreach (ArrayHelper::wrap($characters) as $character) {
57+
$string = preg_replace('/'.preg_quote($character, '/').'+/u', $character, $string);
58+
}
59+
60+
return $string;
61+
}
62+
63+
public static function pluralize(string $value, int|array|Countable $count = 2): string
64+
{
65+
return LanguageHelper::pluralize($value, $count);
66+
}
67+
68+
public static function pluralizeLast(string $value, int|array|Countable $count = 2): string
69+
{
70+
$parts = preg_split('/(.)(?=[A-Z])/u', $value, -1, PREG_SPLIT_DELIM_CAPTURE);
71+
$lastWord = array_pop($parts);
72+
73+
return implode('', $parts) . self::pluralize($lastWord, $count);
74+
}
75+
76+
public static function random(int $length = 16): string
77+
{
78+
$string = '';
79+
80+
while (($len = strlen($string)) < $length) {
81+
$size = $length - $len;
82+
$bytesSize = (int) ceil($size / 3) * 3;
83+
$bytes = random_bytes($bytesSize);
84+
$string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), offset: 0, length: $size);
85+
}
86+
87+
return $string;
88+
}
89+
90+
public static function finish(string $value, string $cap): string
91+
{
92+
return preg_replace('/(?:' . preg_quote($cap, '/') . ')+$/u', replacement: '', subject: $value) . $cap;
93+
}
94+
95+
public static function after(string $subject, string|int $search): string
96+
{
97+
if ($search === '') {
98+
return $subject;
99+
}
100+
101+
return array_reverse(explode((string) $search, $subject, limit: 2))[0];
102+
}
103+
104+
public static function afterLast(string $subject, string|int $search): string
105+
{
106+
if ($search === '') {
107+
return $subject;
108+
}
109+
110+
$position = strrpos($subject, (string) $search);
111+
112+
if ($position === false) {
113+
return $subject;
114+
}
115+
116+
return substr($subject, $position + strlen((string) $search));
117+
}
118+
119+
public static function before(string $subject, string|int $search): string
120+
{
121+
if ($search === '') {
122+
return $subject;
123+
}
124+
125+
$result = strstr($subject, (string) $search, before_needle: true);
126+
127+
if ($result === false) {
128+
return $subject;
129+
}
130+
131+
return $result;
132+
}
133+
134+
public static function beforeLast(string $subject, string|int $search): string
135+
{
136+
if ($search === '') {
137+
return $subject;
138+
}
139+
140+
$pos = mb_strrpos($subject, (string) $search);
141+
142+
if ($pos === false) {
143+
return $subject;
144+
}
145+
146+
return mb_substr($subject, start: 0, length: $pos);
147+
}
148+
149+
public static function between(string $subject, int|string $from, int|string $to): string
150+
{
151+
if ($from === '' || $to === '') {
152+
return $subject;
153+
}
154+
155+
return static::beforeLast(static::after($subject, $from), $to);
156+
}
157+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Support\Tests;
6+
7+
use PHPUnit\Framework\Attributes\TestWith;
8+
use PHPUnit\Framework\TestCase;
9+
use Tempest\Support\StringHelper;
10+
11+
/**
12+
* @internal
13+
* @small
14+
*/
15+
final class StringHelperTest extends TestCase
16+
{
17+
public function test_title(): void
18+
{
19+
$this->assertSame('Jefferson Costella', StringHelper::title('jefferson costella'));
20+
$this->assertSame('Jefferson Costella', StringHelper::title('jefFErson coSTella'));
21+
22+
$this->assertSame('', StringHelper::title(''));
23+
$this->assertSame('123 Tempest', StringHelper::title('123 tempest'));
24+
$this->assertSame('❤Tempest', StringHelper::title('❤tempest'));
25+
$this->assertSame('Tempest ❤', StringHelper::title('tempest ❤'));
26+
$this->assertSame('Tempest123', StringHelper::title('tempest123'));
27+
$this->assertSame('Tempest123', StringHelper::title('Tempest123'));
28+
29+
$longString = 'lorem ipsum '.str_repeat('dolor sit amet ', 1000);
30+
$expectedResult = 'Lorem Ipsum Dolor Sit Amet '.str_repeat('Dolor Sit Amet ', 999);
31+
$this->assertSame($expectedResult, StringHelper::title($longString));
32+
}
33+
34+
public function test_deduplicate(): void
35+
{
36+
$this->assertSame('/some/odd/path/', StringHelper::deduplicate('/some//odd//path/', '/'));
37+
$this->assertSame(' tempest php framework ', StringHelper::deduplicate(' tempest php framework '));
38+
$this->assertSame('what', StringHelper::deduplicate('whaaat', 'a'));
39+
$this->assertSame('ムだム', StringHelper::deduplicate('ムだだム', ''));
40+
}
41+
42+
public function test_pascal(): void
43+
{
44+
$this->assertSame('', StringHelper::pascal(''));
45+
$this->assertSame('FooBar', StringHelper::pascal('foo bar'));
46+
$this->assertSame('FooBar', StringHelper::pascal('foo - bar'));
47+
$this->assertSame('FooBar', StringHelper::pascal('foo__bar'));
48+
$this->assertSame('FooBar', StringHelper::pascal('_foo__bar'));
49+
$this->assertSame('FooBar', StringHelper::pascal('-foo__bar'));
50+
$this->assertSame('FooBar', StringHelper::pascal('fooBar'));
51+
$this->assertSame('FooBar', StringHelper::pascal('foo_bar'));
52+
$this->assertSame('FooBar1', StringHelper::pascal('foo_bar1'));
53+
$this->assertSame('1fooBar', StringHelper::pascal('1foo_bar'));
54+
$this->assertSame('1fooBar11', StringHelper::pascal('1foo_bar11'));
55+
$this->assertSame('1foo1bar1', StringHelper::pascal('1foo_1bar1'));
56+
$this->assertSame('FooBarBaz', StringHelper::pascal('foo-barBaz'));
57+
$this->assertSame('FooBarBaz', StringHelper::pascal('foo-bar_baz'));
58+
// TODO: support when `mb_ucfirst` has landed in PHP 8.4
59+
// $this->assertSame('ÖffentlicheÜberraschungen', StringHelper::pascal('öffentliche-überraschungen'));
60+
}
61+
62+
public function test_kebab(): void
63+
{
64+
$this->assertSame('', StringHelper::kebab(''));
65+
$this->assertSame('foo-bar', StringHelper::kebab('foo bar'));
66+
$this->assertSame('foo-bar', StringHelper::kebab('foo - bar'));
67+
$this->assertSame('foo-bar', StringHelper::kebab('foo__bar'));
68+
$this->assertSame('foo-bar', StringHelper::kebab('_foo__bar'));
69+
$this->assertSame('foo-bar', StringHelper::kebab('-foo__bar'));
70+
$this->assertSame('foo-bar', StringHelper::kebab('fooBar'));
71+
$this->assertSame('foo-bar', StringHelper::kebab('foo_bar'));
72+
$this->assertSame('foo-bar1', StringHelper::kebab('foo_bar1'));
73+
$this->assertSame('1foo-bar', StringHelper::kebab('1foo_bar'));
74+
$this->assertSame('1foo-bar11', StringHelper::kebab('1foo_bar11'));
75+
$this->assertSame('1foo-1bar1', StringHelper::kebab('1foo_1bar1'));
76+
$this->assertSame('foo-bar-baz', StringHelper::kebab('foo-barBaz'));
77+
$this->assertSame('foo-bar-baz', StringHelper::kebab('foo-bar_baz'));
78+
}
79+
80+
public function test_snake(): void
81+
{
82+
$this->assertSame('', StringHelper::snake(''));
83+
$this->assertSame('foo_bar', StringHelper::snake('foo bar'));
84+
$this->assertSame('foo_bar', StringHelper::snake('foo - bar'));
85+
$this->assertSame('foo_bar', StringHelper::snake('foo__bar'));
86+
$this->assertSame('foo_bar', StringHelper::snake('_foo__bar'));
87+
$this->assertSame('foo_bar', StringHelper::snake('-foo__bar'));
88+
$this->assertSame('foo_bar', StringHelper::snake('fooBar'));
89+
$this->assertSame('foo_bar', StringHelper::snake('foo_bar'));
90+
$this->assertSame('foo_bar1', StringHelper::snake('foo_bar1'));
91+
$this->assertSame('1foo_bar', StringHelper::snake('1foo_bar'));
92+
$this->assertSame('1foo_bar11', StringHelper::snake('1foo_bar11'));
93+
$this->assertSame('1foo_1bar1', StringHelper::snake('1foo_1bar1'));
94+
$this->assertSame('foo_bar_baz', StringHelper::snake('foo-barBaz'));
95+
$this->assertSame('foo_bar_baz', StringHelper::snake('foo-bar_baz'));
96+
}
97+
98+
#[TestWith([0])]
99+
#[TestWith([16])]
100+
#[TestWith([100])]
101+
public function test_random(int $length): void
102+
{
103+
$this->assertEquals($length, strlen(StringHelper::random($length)));
104+
}
105+
106+
public function test_finish(): void
107+
{
108+
$this->assertSame('foo/', StringHelper::finish('foo', '/'));
109+
$this->assertSame('foo/', StringHelper::finish('foo/', '/'));
110+
$this->assertSame('abbc', StringHelper::finish('abbcbc', 'bc'));
111+
$this->assertSame('abcbbc', StringHelper::finish('abcbbcbc', 'bc'));
112+
}
113+
114+
public function test_str_after(): void
115+
{
116+
$this->assertSame('nah', StringHelper::after('hannah', 'han'));
117+
$this->assertSame('nah', StringHelper::after('hannah', 'n'));
118+
$this->assertSame('nah', StringHelper::after('ééé hannah', 'han'));
119+
$this->assertSame('hannah', StringHelper::after('hannah', 'xxxx'));
120+
$this->assertSame('hannah', StringHelper::after('hannah', ''));
121+
$this->assertSame('nah', StringHelper::after('han0nah', '0'));
122+
$this->assertSame('nah', StringHelper::after('han0nah', 0));
123+
$this->assertSame('nah', StringHelper::after('han2nah', 2));
124+
}
125+
126+
public function test_str_after_last(): void
127+
{
128+
$this->assertSame('tte', StringHelper::afterLast('yvette', 'yve'));
129+
$this->assertSame('e', StringHelper::afterLast('yvette', 't'));
130+
$this->assertSame('e', StringHelper::afterLast('ééé yvette', 't'));
131+
$this->assertSame('', StringHelper::afterLast('yvette', 'tte'));
132+
$this->assertSame('yvette', StringHelper::afterLast('yvette', 'xxxx'));
133+
$this->assertSame('yvette', StringHelper::afterLast('yvette', ''));
134+
$this->assertSame('te', StringHelper::afterLast('yv0et0te', '0'));
135+
$this->assertSame('te', StringHelper::afterLast('yv0et0te', 0));
136+
$this->assertSame('te', StringHelper::afterLast('yv2et2te', 2));
137+
$this->assertSame('foo', StringHelper::afterLast('----foo', '---'));
138+
}
139+
140+
public function test_str_between(): void
141+
{
142+
$this->assertSame('abc', StringHelper::between('abc', '', 'c'));
143+
$this->assertSame('abc', StringHelper::between('abc', 'a', ''));
144+
$this->assertSame('abc', StringHelper::between('abc', '', ''));
145+
$this->assertSame('b', StringHelper::between('abc', 'a', 'c'));
146+
$this->assertSame('b', StringHelper::between('dddabc', 'a', 'c'));
147+
$this->assertSame('b', StringHelper::between('abcddd', 'a', 'c'));
148+
$this->assertSame('b', StringHelper::between('dddabcddd', 'a', 'c'));
149+
$this->assertSame('nn', StringHelper::between('hannah', 'ha', 'ah'));
150+
$this->assertSame('a]ab[b', StringHelper::between('[a]ab[b]', '[', ']'));
151+
$this->assertSame('foo', StringHelper::between('foofoobar', 'foo', 'bar'));
152+
$this->assertSame('bar', StringHelper::between('foobarbar', 'foo', 'bar'));
153+
$this->assertSame('234', StringHelper::between('12345', 1, 5));
154+
$this->assertSame('45', StringHelper::between('123456789', '123', '6789'));
155+
$this->assertSame('nothing', StringHelper::between('nothing', 'foo', 'bar'));
156+
}
157+
158+
public function test_str_before(): void
159+
{
160+
$this->assertSame('han', StringHelper::before('hannah', 'nah'));
161+
$this->assertSame('ha', StringHelper::before('hannah', 'n'));
162+
$this->assertSame('ééé ', StringHelper::before('ééé hannah', 'han'));
163+
$this->assertSame('hannah', StringHelper::before('hannah', 'xxxx'));
164+
$this->assertSame('hannah', StringHelper::before('hannah', ''));
165+
$this->assertSame('han', StringHelper::before('han0nah', '0'));
166+
$this->assertSame('han', StringHelper::before('han0nah', 0));
167+
$this->assertSame('han', StringHelper::before('han2nah', 2));
168+
$this->assertSame('', StringHelper::before('', ''));
169+
$this->assertSame('', StringHelper::before('', 'a'));
170+
$this->assertSame('', StringHelper::before('a', 'a'));
171+
$this->assertSame('foo', StringHelper::before('[email protected]', '@'));
172+
$this->assertSame('foo', StringHelper::before('foo@@bar.com', '@'));
173+
$this->assertSame('', StringHelper::before('@[email protected]', '@'));
174+
}
175+
176+
public function test_str_before_last(): void
177+
{
178+
$this->assertSame('yve', StringHelper::beforeLast('yvette', 'tte'));
179+
$this->assertSame('yvet', StringHelper::beforeLast('yvette', 't'));
180+
$this->assertSame('ééé ', StringHelper::beforeLast('ééé yvette', 'yve'));
181+
$this->assertSame('', StringHelper::beforeLast('yvette', 'yve'));
182+
$this->assertSame('yvette', StringHelper::beforeLast('yvette', 'xxxx'));
183+
$this->assertSame('yvette', StringHelper::beforeLast('yvette', ''));
184+
$this->assertSame('yv0et', StringHelper::beforeLast('yv0et0te', '0'));
185+
$this->assertSame('yv0et', StringHelper::beforeLast('yv0et0te', 0));
186+
$this->assertSame('yv2et', StringHelper::beforeLast('yv2et2te', 2));
187+
$this->assertSame('', StringHelper::beforeLast('', 'test'));
188+
$this->assertSame('', StringHelper::beforeLast('yvette', 'yvette'));
189+
$this->assertSame('tempest', StringHelper::beforeLast('tempest framework', ' '));
190+
$this->assertSame('yvette', StringHelper::beforeLast("yvette\tyv0et0te", "\t"));
191+
}
192+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Support;
6+
7+
use Tempest\Support\StringHelper;
8+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
9+
10+
/**
11+
* @internal
12+
* @small
13+
*/
14+
final class StringHelperTest extends FrameworkIntegrationTestCase
15+
{
16+
public function test_plural_studly(): void
17+
{
18+
$this->assertSame('RealHumans', StringHelper::pluralizeLast('RealHuman'));
19+
$this->assertSame('Models', StringHelper::pluralizeLast('Model'));
20+
$this->assertSame('VortexFields', StringHelper::pluralizeLast('VortexField'));
21+
$this->assertSame('MultipleWordsInOneStrings', StringHelper::pluralizeLast('MultipleWordsInOneString'));
22+
}
23+
}

0 commit comments

Comments
 (0)