diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a8d4d..9a174b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Requires PHP `8.5` +### Fixed + +- `Str::takeEnd(0)` would return the exact same string instead of an empty string + ## 6.2.0 - 2026-05-14 ### Changed diff --git a/docs/structures/str.md b/docs/structures/str.md index b9a458b..6fe2cf0 100644 --- a/docs/structures/str.md +++ b/docs/structures/str.md @@ -271,6 +271,11 @@ Return a new string with only the n first characters. Str::of('foobar')->take(3)->equals(Str::of('foo')); // true ``` +??? warning + When working with `Encoding::utf8` certain sequences of utf8 characters may lead to unexpected results. + + Make sure to either use it on trusted input or switch to `Encoding::ascii`. + ## `->takeEnd()` Return a new string with only the n last characters. @@ -279,6 +284,11 @@ Return a new string with only the n last characters. Str::of('foobar')->takeEnd(3)->equals(Str::of('bar')); // true ``` +??? warning + When working with `Encoding::utf8` certain sequences of utf8 characters may lead to unexpected results. + + Make sure to either use it on trusted input or switch to `Encoding::ascii`. + ## `->drop()` Return a new string without the n first characters. @@ -287,6 +297,11 @@ Return a new string without the n first characters. Str::of('foobar')->drop(3)->equals(Str::of('bar')); // true ``` +??? warning + When working with `Encoding::utf8` certain sequences of utf8 characters may lead to unexpected results. + + Make sure to either use it on trusted input or switch to `Encoding::ascii`. + ## `->dropEnd()` Return a new string without the n last characters. @@ -295,6 +310,11 @@ Return a new string without the n last characters. Str::of('foobar')->dropEnd(3)->equals(Str::of('foo')); // true ``` +??? warning + When working with `Encoding::utf8` certain sequences of utf8 characters may lead to unexpected results. + + Make sure to either use it on trusted input or switch to `Encoding::ascii`. + ## `->sprintf()` Return a formatted string. diff --git a/proofs/str.php b/proofs/str.php new file mode 100644 index 0000000..1c24bf2 --- /dev/null +++ b/proofs/str.php @@ -0,0 +1,48 @@ +between(0, 100_00) + ->map(Str::of(...)) + ->map(static fn($str) => $str->toEncoding(Str\Encoding::ascii)) + ->map(static fn($str) => static fn() => $str); + $utf8 = Set::strings() + ->madeOf( + Set::strings()->chars()->alphanumerical(), + Set::strings()->unicode()->emoticons(), + Set::strings()->unicode()->alchemicalSymbols(), + Set::strings()->unicode()->mathematicalOperators(), + Set::strings()->unicode()->currencySymbols(), + Set::strings()->unicode()->generalPunctuation(), + Set::strings()->unicode()->arabic(), + Set::strings()->unicode()->hebrew(), + Set::strings()->unicode()->cyrillic(), + Set::strings()->unicode()->controlCharater(), + ) + ->between(0, 100_000) + ->map(Str::of(...)) + ->map(static fn($str) => $str->toEncoding(Str\Encoding::utf8)) + ->map(static fn($str) => static fn() => $str); + $strings = Set::either($ascii, $utf8); + + yield $prove->properties( + 'Str', + Properties::properties(), + $strings, + ); + + foreach (Properties::list() as $property) { + yield $prove->property( + $property, + $strings, + ); + } +}; diff --git a/properties/Str.php b/properties/Str.php new file mode 100644 index 0000000..acb4c75 --- /dev/null +++ b/properties/Str.php @@ -0,0 +1,42 @@ +|Set\Provider + */ + public static function properties(): Set|Set\Provider + { + return Set::properties( + ...\array_map( + static fn($class) => $class::any(), + self::list(), + ), + ); + } + + /** + * @return non-empty-list> + */ + public static function list(): array + { + return [ + Str\Take::class, + Str\TakeEnd::class, + Str\Drop::class, + Str\DropEnd::class, + ]; + } +} diff --git a/properties/Str/Drop.php b/properties/Str/Drop.php new file mode 100644 index 0000000..e5a18ae --- /dev/null +++ b/properties/Str/Drop.php @@ -0,0 +1,53 @@ + + */ +final class Drop implements Property +{ + private function __construct( + private int $size, + ) { + } + + public static function any(): Set + { + // Upper bound is 100 to avoid having too much a probability that the + // resulting Str will be the same one as the input one. + return Set::integers() + ->between(0, 100) + ->map(static fn($size) => new self($size)); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $result = $systemUnderTest->drop($this->size); + $assert->same( + $result->length(), + \max( + 0, + $systemUnderTest->length() - $this->size, + ), + ); + $assert->true( + $systemUnderTest->endsWith($result->toString()), + ); + + return $result; + } +} diff --git a/properties/Str/DropEnd.php b/properties/Str/DropEnd.php new file mode 100644 index 0000000..f86e68d --- /dev/null +++ b/properties/Str/DropEnd.php @@ -0,0 +1,53 @@ + + */ +final class DropEnd implements Property +{ + private function __construct( + private int $size, + ) { + } + + public static function any(): Set + { + // Upper bound is 100 to avoid having too much a probability that the + // resulting Str will be the same one as the input one. + return Set::integers() + ->between(0, 100) + ->map(static fn($size) => new self($size)); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $result = $systemUnderTest->dropEnd($this->size); + $assert->same( + $result->length(), + \max( + 0, + $systemUnderTest->length() - $this->size, + ), + ); + $assert->true( + $systemUnderTest->startsWith($result->toString()), + ); + + return $result; + } +} diff --git a/properties/Str/Take.php b/properties/Str/Take.php new file mode 100644 index 0000000..2e93ebe --- /dev/null +++ b/properties/Str/Take.php @@ -0,0 +1,50 @@ + + */ +final class Take implements Property +{ + private function __construct( + private int $size, + ) { + } + + public static function any(): Set + { + // Upper bound is 100 to avoid having too much a probability that the + // resulting Str will be the same one as the input one. + return Set::integers() + ->between(0, 100) + ->map(static fn($size) => new self($size)); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $result = $systemUnderTest->take($this->size); + $assert + ->number($result->length()) + ->int() + ->lessThanOrEqual($this->size); + $assert->true( + $systemUnderTest->startsWith($result->toString()), + ); + + return $result; + } +} diff --git a/properties/Str/TakeEnd.php b/properties/Str/TakeEnd.php new file mode 100644 index 0000000..f94bebf --- /dev/null +++ b/properties/Str/TakeEnd.php @@ -0,0 +1,50 @@ + + */ +final class TakeEnd implements Property +{ + private function __construct( + private int $size, + ) { + } + + public static function any(): Set + { + // Upper bound is 100 to avoid having too much a probability that the + // resulting Str will be the same one as the input one. + return Set::integers() + ->between(0, 100) + ->map(static fn($size) => new self($size)); + } + + public function applicableTo(object $systemUnderTest): bool + { + return true; + } + + public function ensureHeldBy(Assert $assert, object $systemUnderTest): object + { + $result = $systemUnderTest->takeEnd($this->size); + $assert + ->number($result->length()) + ->int() + ->lessThanOrEqual($this->size); + $assert->true( + $systemUnderTest->endsWith($result->toString()), + ); + + return $result; + } +} diff --git a/src/Str.php b/src/Str.php index d09c0a0..deaf537 100644 --- a/src/Str.php +++ b/src/Str.php @@ -399,6 +399,13 @@ public function substring(int $start, ?int $length = null): self #[\NoDiscard] public function take(int $size): self { + if ($size === 0) { + return new self( + '', + $this->encoding, + ); + } + return $this->substring(0, $size); } @@ -408,6 +415,13 @@ public function take(int $size): self #[\NoDiscard] public function takeEnd(int $size): self { + if ($size === 0) { + return new self( + '', + $this->encoding, + ); + } + return $this->substring(-$size); } @@ -417,6 +431,10 @@ public function takeEnd(int $size): self #[\NoDiscard] public function drop(int $size): self { + if ($size === 0) { + return $this; + } + return $this->substring($size); } @@ -426,6 +444,10 @@ public function drop(int $size): self #[\NoDiscard] public function dropEnd(int $size): self { + if ($size === 0) { + return $this; + } + return $this->substring(0, \max(0, $this->length() - $size)); }