From c149a312d7df67772d1019daaa524ddcfd30a747 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 9 Aug 2026 11:38:18 +0200 Subject: [PATCH 1/3] fix Str::takeEnd(0) returning the same string --- CHANGELOG.md | 4 +++ proofs/str.php | 30 +++++++++++++++++++++ properties/Str.php | 42 ++++++++++++++++++++++++++++++ properties/Str/Drop.php | 53 ++++++++++++++++++++++++++++++++++++++ properties/Str/DropEnd.php | 53 ++++++++++++++++++++++++++++++++++++++ properties/Str/Take.php | 50 +++++++++++++++++++++++++++++++++++ properties/Str/TakeEnd.php | 50 +++++++++++++++++++++++++++++++++++ src/Str.php | 7 +++++ 8 files changed, 289 insertions(+) create mode 100644 proofs/str.php create mode 100644 properties/Str.php create mode 100644 properties/Str/Drop.php create mode 100644 properties/Str/DropEnd.php create mode 100644 properties/Str/Take.php create mode 100644 properties/Str/TakeEnd.php 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/proofs/str.php b/proofs/str.php new file mode 100644 index 0000000..80d77b0 --- /dev/null +++ b/proofs/str.php @@ -0,0 +1,30 @@ +between(0, 100_00) + ->map(Str::of(...)) + ->map(static fn($str) => $str->toEncoding(Str\Encoding::ascii)) + ->map(static fn($str) => static fn() => $str); + + yield $prove->properties( + 'Str', + Properties::properties(), + $strings, + )->tag(\Innmind\BlackBox\Tag::wip); + + foreach (Properties::list() as $property) { + yield $prove->property( + $property, + $strings, + )->tag(\Innmind\BlackBox\Tag::wip); + } +}; 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..db87b15 100644 --- a/src/Str.php +++ b/src/Str.php @@ -408,6 +408,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); } From d51e43f234789eadf87673937dfce70beeb5d4d4 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 9 Aug 2026 13:41:48 +0200 Subject: [PATCH 2/3] make sure Str::take|takeEnd|drop|dropEnd works on unicode characters --- docs/structures/str.md | 20 ++++++++++++++++++++ proofs/str.php | 20 +++++++++++++++++++- src/Str.php | 15 +++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) 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 index 80d77b0..6944633 100644 --- a/proofs/str.php +++ b/proofs/str.php @@ -9,11 +9,29 @@ }; return static function(Prove $prove) { - $strings = Set::strings() + $ascii = Set::strings() ->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', diff --git a/src/Str.php b/src/Str.php index db87b15..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); } @@ -424,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); } @@ -433,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)); } From 0ecc92b55e99b4dd7a61a0aa43b6fa131d72bd58 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 9 Aug 2026 13:43:35 +0200 Subject: [PATCH 3/3] remove wip tags --- proofs/str.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proofs/str.php b/proofs/str.php index 6944633..1c24bf2 100644 --- a/proofs/str.php +++ b/proofs/str.php @@ -37,12 +37,12 @@ 'Str', Properties::properties(), $strings, - )->tag(\Innmind\BlackBox\Tag::wip); + ); foreach (Properties::list() as $property) { yield $prove->property( $property, $strings, - )->tag(\Innmind\BlackBox\Tag::wip); + ); } };