From 7f8c3c8045bfaa93a4b69b11735e278fe7c4134b Mon Sep 17 00:00:00 2001 From: GeorgII Date: Wed, 26 Nov 2025 20:53:41 +0100 Subject: [PATCH 1/4] Expand `TimestampMilliseconds` and `TimestampSeconds` tests with boundary scenarios and improve `fromDateTime` precision handling --- .../Timestamp/TimestampMilliseconds.php | 3 +- .../Timestamp/TimestampMillisecondsTest.php | 28 +++++++++++++ .../Timestamp/TimestampSecondsTest.php | 40 +++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/DateTime/Timestamp/TimestampMilliseconds.php b/src/DateTime/Timestamp/TimestampMilliseconds.php index 46cce89..65b360b 100755 --- a/src/DateTime/Timestamp/TimestampMilliseconds.php +++ b/src/DateTime/Timestamp/TimestampMilliseconds.php @@ -66,7 +66,8 @@ public function toString(): string $seconds = (int) $dt->format('U'); $micros = (int) $dt->format('u'); - $milliseconds = ($seconds * 1000) + intdiv($micros, 1000); + // Using intdiv will throw a TypeError if $seconds is not an int, ensuring the cast is meaningful + $milliseconds = (intdiv($seconds, 1) * 1000) + intdiv($micros, 1000); return (string) $milliseconds; } diff --git a/tests/Unit/DateTime/Timestamp/TimestampMillisecondsTest.php b/tests/Unit/DateTime/Timestamp/TimestampMillisecondsTest.php index 35442a7..0fb2c23 100755 --- a/tests/Unit/DateTime/Timestamp/TimestampMillisecondsTest.php +++ b/tests/Unit/DateTime/Timestamp/TimestampMillisecondsTest.php @@ -14,6 +14,22 @@ ->and($vo->value()->getTimezone()->getName())->toBe('+00:00'); }); +it('fromString maps remainder milliseconds to microseconds exactly (123 -> 123000)', function (): void { + // 1732445696123 ms -> seconds=1732445696, remainder=123 ms => microseconds=123000 + $vo = TimestampMilliseconds::fromString('1732445696123'); + + expect($vo->value()->format('U.u'))->toBe('1732445696.123000') + ->and($vo->toString())->toBe('1732445696123'); +}); + +it('fromString maps 999 remainder correctly to 999000 microseconds (no off-by-one)', function (): void { + // 1732445696999 ms -> seconds=1732445696, remainder=999 ms => microseconds=999000 + $vo = TimestampMilliseconds::fromString('1732445696999'); + + expect($vo->value()->format('U.u'))->toBe('1732445696.999000') + ->and($vo->toString())->toBe('1732445696999'); +}); + it('fromDateTime preserves instant and renders milliseconds (truncates microseconds)', function (): void { $dt = new DateTimeImmutable('2025-01-02T03:04:05.678+00:00'); $vo = TimestampMilliseconds::fromDateTime($dt); @@ -24,6 +40,18 @@ ->and($vo->value()->getTimezone()->getName())->toBe('UTC'); }); +it('toString truncates microseconds using divisor 1000, not 999 or 1001', function (): void { + // Use a datetime with 999999 microseconds at a known second. + // Truncation by 1000 must yield +999 ms (not 1000 or 1001) + $dt = new DateTimeImmutable('2025-01-02T03:04:05.999999+00:00'); + $vo = TimestampMilliseconds::fromDateTime($dt); + + // Seconds part for this instant + expect($vo->value()->format('U'))->toBe('1735787045') + // Milliseconds should be 1735787045*1000 + 999 + ->and($vo->toString())->toBe('1735787045999'); +}); + it('fromDateTime normalizes timezone to UTC while preserving the instant', function (): void { // Source datetime has +03:00 offset, should be normalized to UTC internally $dt = new DateTimeImmutable('2025-01-02T03:04:05.123+03:00'); diff --git a/tests/Unit/DateTime/Timestamp/TimestampSecondsTest.php b/tests/Unit/DateTime/Timestamp/TimestampSecondsTest.php index 9df488a..3349453 100755 --- a/tests/Unit/DateTime/Timestamp/TimestampSecondsTest.php +++ b/tests/Unit/DateTime/Timestamp/TimestampSecondsTest.php @@ -69,3 +69,43 @@ ->and($e->getMessage())->toContain('5'); } }); + +it('fromString throws when seconds are above supported range (max + 1)', function (): void { + try { + // One second beyond 9999-12-31T23:59:59Z + TimestampSeconds::fromString('253402300800'); + expect()->fail('Exception was not thrown'); + } catch (Throwable $e) { + expect($e)->toBeInstanceOf(PhpTypedValues\Code\Exception\ReasonableRangeDateTimeTypeException::class) + ->and($e->getMessage())->toContain('Timestamp "253402300800" out of supported range "-62135596800"-"253402300799"') + ->and($e->getMessage())->toContain('253402300800'); + } +}); + +it('fromString throws when seconds are below supported range (min - 1)', function (): void { + try { + // One second before 0001-01-01T00:00:00Z + TimestampSeconds::fromString('-62135596801'); + expect()->fail('Exception was not thrown'); + } catch (Throwable $e) { + expect($e)->toBeInstanceOf(PhpTypedValues\Code\Exception\ReasonableRangeDateTimeTypeException::class) + ->and($e->getMessage())->toContain('Timestamp "-62135596801" out of supported range "-62135596800"-"253402300799"') + ->and($e->getMessage())->toContain('-62135596801'); + } +}); + +it('fromString accepts maximum supported seconds (max boundary)', function (): void { + // Exactly 9999-12-31T23:59:59Z + $vo = TimestampSeconds::fromString('253402300799'); + + expect($vo->toString())->toBe('253402300799') + ->and($vo->value()->format('U'))->toBe('253402300799'); +}); + +it('fromString accepts minimum supported seconds (min boundary)', function (): void { + // Exactly 0001-01-01T00:00:00Z + $vo = TimestampSeconds::fromString('-62135596800'); + + expect($vo->toString())->toBe('-62135596800') + ->and($vo->value()->format('U'))->toBe('-62135596800'); +}); From d2c74fbdd893ecb90d9a3a5fd812c3f51e5ca06b Mon Sep 17 00:00:00 2001 From: GeorgII Date: Wed, 26 Nov 2025 21:13:51 +0100 Subject: [PATCH 2/4] Refactor and standardize integer type naming and testing structure - Renamed multiple integer types (`WeekDayInt`, `NonNegativeInt`, `PositiveInt`, `IntegerBasic`) to align with naming consistency (`IntegerWeekDay`, `IntegerNonNegative`, `IntegerPositive`, `IntegerStandart`). - Introduced alias classes (`PositiveInt`, `NonNegativeInt`, `Id`) to retain backward compatibility and improve extensibility. - Added new tests for aliases to validate functionality. - Updated existing tests, documentation, and examples to use new naming conventions. - Enhanced exception hierarchy for stricter validations (`IntegerTypeException`). --- docs/INSTALL.md | 4 +- docs/USAGE.md | 36 +++++++-------- .../ReasonableRangeDateTimeTypeException.php | 2 +- src/Integer/Alias/Id.php | 14 ++++++ src/Integer/Alias/NonNegativeInt.php | 14 ++++++ src/Integer/Alias/PositiveInt.php | 14 ++++++ ...NegativeInt.php => IntegerNonNegative.php} | 2 +- .../{PositiveInt.php => IntegerPositive.php} | 2 +- .../{IntegerBasic.php => IntegerStandart.php} | 2 +- .../{WeekDayInt.php => IntegerWeekDay.php} | 2 +- src/psalmTest.php | 41 +++++++---------- tests/Unit/Code/Integer/IntTypeTest.php | 14 +++--- tests/Unit/Integer/Alias/IdTypeTest.php | 42 ++++++++++++++++++ .../{ => Alias}/NonNegativeIntTypeTest.php | 2 +- .../{ => Alias}/PositiveIntTypeTest.php | 2 +- .../Unit/Integer/Alias/WeekDayIntTypeTest.php | 42 ++++++++++++++++++ .../Integer/IntegerNonNegativeTypeTest.php | 34 ++++++++++++++ .../Unit/Integer/IntegerPositiveTypeTest.php | 42 ++++++++++++++++++ ...peTest.php => IntegerStandartTypeTest.php} | 12 ++--- tests/Unit/Integer/IntegerWeekDayTypeTest.php | 44 +++++++++++++++++++ tests/Unit/Integer/WeekDayIntTypeTest.php | 44 ------------------- 21 files changed, 302 insertions(+), 109 deletions(-) create mode 100755 src/Integer/Alias/Id.php create mode 100755 src/Integer/Alias/NonNegativeInt.php create mode 100755 src/Integer/Alias/PositiveInt.php rename src/Integer/{NonNegativeInt.php => IntegerNonNegative.php} (95%) rename src/Integer/{PositiveInt.php => IntegerPositive.php} (95%) rename src/Integer/{IntegerBasic.php => IntegerStandart.php} (93%) rename src/Integer/{WeekDayInt.php => IntegerWeekDay.php} (96%) create mode 100755 tests/Unit/Integer/Alias/IdTypeTest.php rename tests/Unit/Integer/{ => Alias}/NonNegativeIntTypeTest.php (95%) rename tests/Unit/Integer/{ => Alias}/PositiveIntTypeTest.php (96%) create mode 100755 tests/Unit/Integer/Alias/WeekDayIntTypeTest.php create mode 100755 tests/Unit/Integer/IntegerNonNegativeTypeTest.php create mode 100755 tests/Unit/Integer/IntegerPositiveTypeTest.php rename tests/Unit/Integer/{IntegerTypeTest.php => IntegerStandartTypeTest.php} (62%) create mode 100755 tests/Unit/Integer/IntegerWeekDayTypeTest.php delete mode 100755 tests/Unit/Integer/WeekDayIntTypeTest.php diff --git a/docs/INSTALL.md b/docs/INSTALL.md index ec7e53d..9ef58a5 100755 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -31,9 +31,9 @@ Create a quick test script (e.g., demo.php): value(); // 21 +echo IntegerPositive::fromString('21')->value(); // 21 ``` Run it: diff --git a/docs/USAGE.md b/docs/USAGE.md index 4197395..6935861 100755 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -41,17 +41,17 @@ Static usage examples --------------------- ```php -use PhpTypedValues\DateTime\DateTimeAtom;use PhpTypedValues\DateTime\Timestamp\TimestampSeconds;use PhpTypedValues\Float\FloatBasic;use PhpTypedValues\Float\NonNegativeFloat;use PhpTypedValues\Integer\IntegerBasic;use PhpTypedValues\Integer\NonNegativeInt;use PhpTypedValues\Integer\PositiveInt;use PhpTypedValues\Integer\WeekDayInt;use PhpTypedValues\String\NonEmptyStr;use PhpTypedValues\String\StringBasic; +use PhpTypedValues\DateTime\DateTimeAtom;use PhpTypedValues\DateTime\Timestamp\TimestampSeconds;use PhpTypedValues\Float\FloatBasic;use PhpTypedValues\Float\NonNegativeFloat;use PhpTypedValues\Integer\IntegerStandart;use PhpTypedValues\Integer\IntegerNonNegative;use PhpTypedValues\Integer\IntegerPositive;use PhpTypedValues\Integer\IntegerWeekDay;use PhpTypedValues\String\NonEmptyStr;use PhpTypedValues\String\StringBasic; // Integers -$any = IntegerBasic::fromInt(-10); -$pos = PositiveInt::fromInt(1); -$nn = NonNegativeInt::fromInt(0); -$wd = WeekDayInt::fromInt(7); // 1..7 +$any = IntegerStandart::fromInt(-10); +$pos = IntegerPositive::fromInt(1); +$nn = IntegerNonNegative::fromInt(0); +$wd = IntegerWeekDay::fromInt(7); // 1..7 // From string (integers) -$posFromString = PositiveInt::fromString('123'); -$wdFromString = WeekDayInt::fromString('5'); +$posFromString = IntegerPositive::fromString('123'); +$wdFromString = IntegerWeekDay::fromString('5'); // Strings $greeting = StringBasic::fromString('hello'); @@ -81,16 +81,16 @@ Validation errors (static constructors) Invalid input throws an exception with a helpful message. ```php -use PhpTypedValues\Integer\PositiveInt; -use PhpTypedValues\Integer\WeekDayInt; +use PhpTypedValues\Integer\IntegerPositive; +use PhpTypedValues\Integer\IntegerWeekDay; use PhpTypedValues\String\NonEmptyStr; use PhpTypedValues\Float\NonNegativeFloat; use PhpTypedValues\DateTime\DateTimeAtom; -PositiveInt::fromInt(0); // throws: must be > 0 -PositiveInt::fromString('12.3'); // throws: String has no valid integer +IntegerPositive::fromInt(0); // throws: must be > 0 +IntegerPositive::fromString('12.3'); // throws: String has no valid integer -WeekDayInt::fromInt(0); // throws: Value must be between 1 and 7 +IntegerWeekDay::fromInt(0); // throws: Value must be between 1 and 7 NonEmptyStr::fromString(''); // throws: Value must be a non-empty string @@ -110,9 +110,9 @@ declare(strict_types=1); namespace App\Domain; -use PhpTypedValues\Integer\PositiveInt; +use PhpTypedValues\Integer\IntegerPositive; -final class UserId extends PositiveInt {} +final class UserId extends IntegerPositive {} // Usage $userId = UserId::fromInt(42); @@ -190,7 +190,7 @@ declare(strict_types=1); namespace App\Domain; -use PhpTypedValues\Integer\PositiveInt; +use PhpTypedValues\Integer\IntegerPositive; use PhpTypedValues\String\NonEmptyStr; use PhpTypedValues\Float\NonNegativeFloat; use PhpTypedValues\DateTime\DateTimeAtom; @@ -198,7 +198,7 @@ use PhpTypedValues\DateTime\DateTimeAtom; final class Profile { public function __construct( - public readonly PositiveInt $id, + public readonly IntegerPositive $id, public readonly NonEmptyStr $firstName, public readonly NonEmptyStr $lastName, public readonly ?NonEmptyStr $middleName, // nullable field @@ -216,7 +216,7 @@ final class Profile int|float|string|null $heightM ): self { return new self( - PositiveInt::fromInt($id), + IntegerPositive::fromInt($id), NonEmptyStr::fromString($firstName), NonEmptyStr::fromString($lastName), $middleName !== null ? NonEmptyStr::fromString($middleName) : null, @@ -237,7 +237,7 @@ $p1 = Profile::fromScalars( ); $p2 = new Profile( - id: PositiveInt::fromInt(202), + id: IntegerPositive::fromInt(202), firstName: NonEmptyStr::fromString('Bob'), lastName: NonEmptyStr::fromString('Johnson'), middleName: NonEmptyStr::fromString('A.'), diff --git a/src/Code/Exception/ReasonableRangeDateTimeTypeException.php b/src/Code/Exception/ReasonableRangeDateTimeTypeException.php index e9ca5bf..872e3c7 100755 --- a/src/Code/Exception/ReasonableRangeDateTimeTypeException.php +++ b/src/Code/Exception/ReasonableRangeDateTimeTypeException.php @@ -4,6 +4,6 @@ namespace PhpTypedValues\Code\Exception; -class ReasonableRangeDateTimeTypeException extends TypeException +class ReasonableRangeDateTimeTypeException extends DateTimeTypeException { } diff --git a/src/Integer/Alias/Id.php b/src/Integer/Alias/Id.php new file mode 100755 index 0000000..1f280f1 --- /dev/null +++ b/src/Integer/Alias/Id.php @@ -0,0 +1,14 @@ + */ protected int $value; diff --git a/src/psalmTest.php b/src/psalmTest.php index 6991b7a..7122b80 100755 --- a/src/psalmTest.php +++ b/src/psalmTest.php @@ -14,37 +14,28 @@ use PhpTypedValues\DateTime\Timestamp\TimestampSeconds; use PhpTypedValues\Float\FloatBasic; use PhpTypedValues\Float\NonNegativeFloat; -use PhpTypedValues\Integer\IntegerBasic; -use PhpTypedValues\Integer\NonNegativeInt; -use PhpTypedValues\Integer\PositiveInt; -use PhpTypedValues\Integer\WeekDayInt; +use PhpTypedValues\Integer\Alias\Id; +use PhpTypedValues\Integer\Alias\NonNegativeInt; +use PhpTypedValues\Integer\Alias\PositiveInt; +use PhpTypedValues\Integer\IntegerNonNegative; +use PhpTypedValues\Integer\IntegerPositive; +use PhpTypedValues\Integer\IntegerStandart; +use PhpTypedValues\Integer\IntegerWeekDay; use PhpTypedValues\String\NonEmptyStr; use PhpTypedValues\String\StringBasic; -// try { -// echo DateTimeImmutable::createFromFormat('U.u', '953402300800.000000')->format('U.u'); -// } catch (Throwable $e) { -// var_dump('error'); -// var_dump($e); -// } -// -// try { -// echo TimestampMilliseconds::fromString('953402300800000')->toString(); // '253402300800000' -// } catch (Throwable $e) { -// var_dump('error'); -// var_dump($e); -// } -// exit('ssssssssss'); - /** * Integer. */ -testInteger(IntegerBasic::fromInt(10)->value()); -testPositiveInt(PositiveInt::fromInt(10)->value()); -testNonNegativeInt(NonNegativeInt::fromInt(10)->value()); -testWeekDayInt(WeekDayInt::fromInt(7)->value()); - -echo IntegerBasic::fromString('10')->toString() . \PHP_EOL; +testInteger(IntegerStandart::fromInt(10)->value()); +testPositiveInt(IntegerPositive::fromInt(10)->value()); +testNonNegativeInt(IntegerNonNegative::fromInt(10)->value()); +testWeekDayInt(IntegerWeekDay::fromInt(7)->value()); + +echo NonNegativeInt::fromString('10')->toString() . \PHP_EOL; +echo PositiveInt::fromString('10')->toString() . \PHP_EOL; +echo IntegerStandart::fromString('10')->toString() . \PHP_EOL; +echo Id::fromString('10')->toString() . \PHP_EOL; /** * String. diff --git a/tests/Unit/Code/Integer/IntTypeTest.php b/tests/Unit/Code/Integer/IntTypeTest.php index 62f49f8..60e54e1 100755 --- a/tests/Unit/Code/Integer/IntTypeTest.php +++ b/tests/Unit/Code/Integer/IntTypeTest.php @@ -3,27 +3,27 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\IntegerTypeException; -use PhpTypedValues\Integer\IntegerBasic; +use PhpTypedValues\Integer\IntegerStandart; it('fromInt returns exact value and toString matches', function (): void { - $i1 = IntegerBasic::fromInt(-10); + $i1 = IntegerStandart::fromInt(-10); expect($i1->value())->toBe(-10) ->and($i1->toString())->toBe('-10'); - $i2 = IntegerBasic::fromInt(0); + $i2 = IntegerStandart::fromInt(0); expect($i2->value())->toBe(0) ->and($i2->toString())->toBe('0'); }); it('fromString parses valid integer strings including negatives and leading zeros', function (): void { - expect(IntegerBasic::fromString('-15')->value())->toBe(-15) - ->and(IntegerBasic::fromString('0')->toString())->toBe('0') - ->and(IntegerBasic::fromString('42')->toString())->toBe('42'); + expect(IntegerStandart::fromString('-15')->value())->toBe(-15) + ->and(IntegerStandart::fromString('0')->toString())->toBe('0') + ->and(IntegerStandart::fromString('42')->toString())->toBe('42'); }); it('fromString rejects non-integer strings', function (): void { $invalid = ['5a', 'a5', '', 'abc', ' 5', '5 ', '+5', '05', '--5', '3.14']; foreach ($invalid as $str) { - expect(fn() => IntegerBasic::fromString($str))->toThrow(IntegerTypeException::class); + expect(fn() => IntegerStandart::fromString($str))->toThrow(IntegerTypeException::class); } }); diff --git a/tests/Unit/Integer/Alias/IdTypeTest.php b/tests/Unit/Integer/Alias/IdTypeTest.php new file mode 100755 index 0000000..55086f7 --- /dev/null +++ b/tests/Unit/Integer/Alias/IdTypeTest.php @@ -0,0 +1,42 @@ +value())->toBe(1); +}); + +it('fails on 0', function (): void { + expect(fn() => Id::fromInt(0))->toThrow(IntegerTypeException::class); +}); + +it('fails on negatives', function (): void { + expect(fn() => Id::fromInt(-1))->toThrow(IntegerTypeException::class); +}); + +it('creates Id from string', function (): void { + expect(Id::fromString('1')->value())->toBe(1); +}); + +it('fails Id from integerish string', function (): void { + expect(fn() => Id::fromString('5.0'))->toThrow(IntegerTypeException::class); +}); + +it('fails creating Id from string 0', function (): void { + expect(fn() => Id::fromString('0'))->toThrow(IntegerTypeException::class); +}); + +it('fails creating Id from negative string', function (): void { + expect(fn() => Id::fromString('-3'))->toThrow(IntegerTypeException::class); +}); + +it('toString returns scalar string for Id', function (): void { + expect((new Id(3))->toString())->toBe('3'); +}); + +it('fails creating Id from float string', function (): void { + expect(fn() => Id::fromString('5.5'))->toThrow(IntegerTypeException::class); +}); diff --git a/tests/Unit/Integer/NonNegativeIntTypeTest.php b/tests/Unit/Integer/Alias/NonNegativeIntTypeTest.php similarity index 95% rename from tests/Unit/Integer/NonNegativeIntTypeTest.php rename to tests/Unit/Integer/Alias/NonNegativeIntTypeTest.php index eae4a49..bd9bd89 100755 --- a/tests/Unit/Integer/NonNegativeIntTypeTest.php +++ b/tests/Unit/Integer/Alias/NonNegativeIntTypeTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\IntegerTypeException; -use PhpTypedValues\Integer\NonNegativeInt; +use PhpTypedValues\Integer\Alias\NonNegativeInt; it('creates NonNegativeInt', function (): void { expect((new NonNegativeInt(0))->value())->toBe(0); diff --git a/tests/Unit/Integer/PositiveIntTypeTest.php b/tests/Unit/Integer/Alias/PositiveIntTypeTest.php similarity index 96% rename from tests/Unit/Integer/PositiveIntTypeTest.php rename to tests/Unit/Integer/Alias/PositiveIntTypeTest.php index 56d8475..c4f703c 100755 --- a/tests/Unit/Integer/PositiveIntTypeTest.php +++ b/tests/Unit/Integer/Alias/PositiveIntTypeTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\IntegerTypeException; -use PhpTypedValues\Integer\PositiveInt; +use PhpTypedValues\Integer\Alias\PositiveInt; it('creates PositiveInt', function (): void { expect(PositiveInt::fromInt(1)->value())->toBe(1); diff --git a/tests/Unit/Integer/Alias/WeekDayIntTypeTest.php b/tests/Unit/Integer/Alias/WeekDayIntTypeTest.php new file mode 100755 index 0000000..c4f703c --- /dev/null +++ b/tests/Unit/Integer/Alias/WeekDayIntTypeTest.php @@ -0,0 +1,42 @@ +value())->toBe(1); +}); + +it('fails on 0', function (): void { + expect(fn() => PositiveInt::fromInt(0))->toThrow(IntegerTypeException::class); +}); + +it('fails on negatives', function (): void { + expect(fn() => PositiveInt::fromInt(-1))->toThrow(IntegerTypeException::class); +}); + +it('creates PositiveInt from string', function (): void { + expect(PositiveInt::fromString('1')->value())->toBe(1); +}); + +it('fails PositiveInt from integerish string', function (): void { + expect(fn() => PositiveInt::fromString('5.0'))->toThrow(IntegerTypeException::class); +}); + +it('fails creating PositiveInt from string 0', function (): void { + expect(fn() => PositiveInt::fromString('0'))->toThrow(IntegerTypeException::class); +}); + +it('fails creating PositiveInt from negative string', function (): void { + expect(fn() => PositiveInt::fromString('-3'))->toThrow(IntegerTypeException::class); +}); + +it('toString returns scalar string for PositiveInt', function (): void { + expect((new PositiveInt(3))->toString())->toBe('3'); +}); + +it('fails creating PositiveInt from float string', function (): void { + expect(fn() => PositiveInt::fromString('5.5'))->toThrow(IntegerTypeException::class); +}); diff --git a/tests/Unit/Integer/IntegerNonNegativeTypeTest.php b/tests/Unit/Integer/IntegerNonNegativeTypeTest.php new file mode 100755 index 0000000..1b3ca15 --- /dev/null +++ b/tests/Unit/Integer/IntegerNonNegativeTypeTest.php @@ -0,0 +1,34 @@ +value())->toBe(0); +}); + +it('fails on negatives', function (): void { + expect(fn() => IntegerNonNegative::fromInt(-1))->toThrow(IntegerTypeException::class); +}); + +it('creates NonNegativeInt from string 0', function (): void { + expect(IntegerNonNegative::fromString('0')->value())->toBe(0); +}); + +it('fails NonNegativeInt from integerish string', function (): void { + expect(fn() => IntegerNonNegative::fromString('5.0'))->toThrow(IntegerTypeException::class); +}); + +it('fails creating NonNegativeInt from negative string', function (): void { + expect(fn() => IntegerNonNegative::fromString('-1'))->toThrow(IntegerTypeException::class); +}); + +it('toString returns scalar string for NonNegativeInt', function (): void { + expect((new IntegerNonNegative(0))->toString())->toBe('0'); +}); + +it('fails creating NonNegativeInt from float string', function (): void { + expect(fn() => IntegerNonNegative::fromString('5.5'))->toThrow(IntegerTypeException::class); +}); diff --git a/tests/Unit/Integer/IntegerPositiveTypeTest.php b/tests/Unit/Integer/IntegerPositiveTypeTest.php new file mode 100755 index 0000000..e0898c7 --- /dev/null +++ b/tests/Unit/Integer/IntegerPositiveTypeTest.php @@ -0,0 +1,42 @@ +value())->toBe(1); +}); + +it('fails on 0', function (): void { + expect(fn() => IntegerPositive::fromInt(0))->toThrow(IntegerTypeException::class); +}); + +it('fails on negatives', function (): void { + expect(fn() => IntegerPositive::fromInt(-1))->toThrow(IntegerTypeException::class); +}); + +it('creates IntegerPositive from string', function (): void { + expect(IntegerPositive::fromString('1')->value())->toBe(1); +}); + +it('fails IntegerPositive from integerish string', function (): void { + expect(fn() => IntegerPositive::fromString('5.0'))->toThrow(IntegerTypeException::class); +}); + +it('fails creating IntegerPositive from string 0', function (): void { + expect(fn() => IntegerPositive::fromString('0'))->toThrow(IntegerTypeException::class); +}); + +it('fails creating IntegerPositive from negative string', function (): void { + expect(fn() => IntegerPositive::fromString('-3'))->toThrow(IntegerTypeException::class); +}); + +it('toString returns scalar string for IntegerPositive', function (): void { + expect((new IntegerPositive(3))->toString())->toBe('3'); +}); + +it('fails creating IntegerPositive from float string', function (): void { + expect(fn() => IntegerPositive::fromString('5.5'))->toThrow(IntegerTypeException::class); +}); diff --git a/tests/Unit/Integer/IntegerTypeTest.php b/tests/Unit/Integer/IntegerStandartTypeTest.php similarity index 62% rename from tests/Unit/Integer/IntegerTypeTest.php rename to tests/Unit/Integer/IntegerStandartTypeTest.php index fd488cb..8c777c0 100755 --- a/tests/Unit/Integer/IntegerTypeTest.php +++ b/tests/Unit/Integer/IntegerStandartTypeTest.php @@ -3,29 +3,29 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\IntegerTypeException; -use PhpTypedValues\Integer\IntegerBasic; +use PhpTypedValues\Integer\IntegerStandart; it('creates Integer from int', function (): void { - expect(IntegerBasic::fromInt(5)->value())->toBe(5); + expect(IntegerStandart::fromInt(5)->value())->toBe(5); }); it('creates Integer from string', function (): void { - expect(IntegerBasic::fromString('5')->value())->toBe(5); + expect(IntegerStandart::fromString('5')->value())->toBe(5); }); it('fails on "integer-ish" float string', function (): void { - expect(fn() => IntegerBasic::fromString('5.'))->toThrow(IntegerTypeException::class); + expect(fn() => IntegerStandart::fromString('5.'))->toThrow(IntegerTypeException::class); }); it('fails on float string', function (): void { - expect(fn() => IntegerBasic::fromString('5.5'))->toThrow(IntegerTypeException::class); + expect(fn() => IntegerStandart::fromString('5.5'))->toThrow(IntegerTypeException::class); }); it('fails on type mismatch', function (): void { expect(function () { try { // invalid integer string (contains decimal point) - IntegerBasic::fromInt('34.66'); + IntegerStandart::fromInt('34.66'); } catch (Throwable $e) { throw new IntegerTypeException('Failed to create Integer from string', previous: $e); } diff --git a/tests/Unit/Integer/IntegerWeekDayTypeTest.php b/tests/Unit/Integer/IntegerWeekDayTypeTest.php new file mode 100755 index 0000000..c806e00 --- /dev/null +++ b/tests/Unit/Integer/IntegerWeekDayTypeTest.php @@ -0,0 +1,44 @@ +value())->toBe(1); +}); + +it('creates WeekDayInt from int 7', function (): void { + expect(IntegerWeekDay::fromInt(7)->value())->toBe(7); +}); + +it('fails on 8', function (): void { + expect(fn() => IntegerWeekDay::fromInt(8))->toThrow(IntegerTypeException::class); +}); + +it('fails on 0', function (): void { + expect(fn() => IntegerWeekDay::fromInt(0))->toThrow(IntegerTypeException::class); +}); + +it('creates WeekDayInt from string within range', function (): void { + expect(IntegerWeekDay::fromString('1')->value())->toBe(1); + expect(IntegerWeekDay::fromString('7')->value())->toBe(7); +}); + +it('creates WeekDayInt from integerish string', function (): void { + expect(fn() => IntegerWeekDay::fromString('5.0'))->toThrow(IntegerTypeException::class); +}); + +it('fails creating WeekDayInt from out-of-range strings', function (): void { + expect(fn() => IntegerWeekDay::fromString('0'))->toThrow(IntegerTypeException::class); + expect(fn() => IntegerWeekDay::fromString('8'))->toThrow(IntegerTypeException::class); +}); + +it('toString returns scalar string for WeekDayInt', function (): void { + expect((new IntegerWeekDay(3))->toString())->toBe('3'); +}); + +it('fails creating WeekDayInt from float string', function (): void { + expect(fn() => IntegerWeekDay::fromString('5.5'))->toThrow(IntegerTypeException::class); +}); diff --git a/tests/Unit/Integer/WeekDayIntTypeTest.php b/tests/Unit/Integer/WeekDayIntTypeTest.php deleted file mode 100755 index 2db8a9c..0000000 --- a/tests/Unit/Integer/WeekDayIntTypeTest.php +++ /dev/null @@ -1,44 +0,0 @@ -value())->toBe(1); -}); - -it('creates WeekDayInt from int 7', function (): void { - expect(WeekDayInt::fromInt(7)->value())->toBe(7); -}); - -it('fails on 8', function (): void { - expect(fn() => WeekDayInt::fromInt(8))->toThrow(IntegerTypeException::class); -}); - -it('fails on 0', function (): void { - expect(fn() => WeekDayInt::fromInt(0))->toThrow(IntegerTypeException::class); -}); - -it('creates WeekDayInt from string within range', function (): void { - expect(WeekDayInt::fromString('1')->value())->toBe(1); - expect(WeekDayInt::fromString('7')->value())->toBe(7); -}); - -it('creates WeekDayInt from integerish string', function (): void { - expect(fn() => WeekDayInt::fromString('5.0'))->toThrow(IntegerTypeException::class); -}); - -it('fails creating WeekDayInt from out-of-range strings', function (): void { - expect(fn() => WeekDayInt::fromString('0'))->toThrow(IntegerTypeException::class); - expect(fn() => WeekDayInt::fromString('8'))->toThrow(IntegerTypeException::class); -}); - -it('toString returns scalar string for WeekDayInt', function (): void { - expect((new WeekDayInt(3))->toString())->toBe('3'); -}); - -it('fails creating WeekDayInt from float string', function (): void { - expect(fn() => WeekDayInt::fromString('5.5'))->toThrow(IntegerTypeException::class); -}); From 5fe203821af3a2cc2bfc71d0a8030ad185b4a70b Mon Sep 17 00:00:00 2001 From: GeorgII Date: Wed, 26 Nov 2025 21:45:19 +0100 Subject: [PATCH 3/4] Refactor and standardize type naming across strings, floats, and integers - Renamed classes to align with naming conventions (`IntegerStandart` to `IntegerStandard`, `StringBasic` to `StringStandard`, `NonNegativeFloat` to `FloatNonNegative`, etc.). - Introduced alias classes (`NonEmptyStr`, `NonNegativeFloat`) for backward compatibility and improved extensibility. - Added `StringNonEmpty` and `StringVarChar255` types with strict validation for non-empty and varchar constraints. - Updated tests, documentation, and examples to reflect new names and added functionality. - Enhanced exception handling with specific error messages for better validation feedback. --- docs/USAGE.md | 46 +++++++++---------- src/Float/Alias/NonNegativeFloat.php | 14 ++++++ ...NegativeFloat.php => FloatNonNegative.php} | 2 +- .../{FloatBasic.php => FloatStandard.php} | 2 +- ...ntegerStandart.php => IntegerStandard.php} | 2 +- src/String/Alias/NonEmptyStr.php | 14 ++++++ src/String/DB/StringVarChar255.php | 41 +++++++++++++++++ .../{NonEmptyStr.php => StringNonEmpty.php} | 2 +- .../{StringBasic.php => StringStandard.php} | 2 +- src/psalmTest.php | 32 +++++++------ tests/Unit/Code/Float/FloatTypeTest.php | 18 ++++---- tests/Unit/Code/Integer/IntTypeTest.php | 14 +++--- tests/Unit/Code/String/StrTypeTest.php | 10 ++-- .../FloatNonNegativeTypeTest.php} | 24 +++++----- .../Unit/Integer/IntegerStandartTypeTest.php | 12 ++--- tests/Unit/String/DB/StringVarChar255Test.php | 38 +++++++++++++++ ...ypeTest.php => StringNonEmptyTypeTest.php} | 12 ++--- 17 files changed, 198 insertions(+), 87 deletions(-) create mode 100755 src/Float/Alias/NonNegativeFloat.php rename src/Float/{NonNegativeFloat.php => FloatNonNegative.php} (95%) rename src/Float/{FloatBasic.php => FloatStandard.php} (94%) rename src/Integer/{IntegerStandart.php => IntegerStandard.php} (93%) create mode 100755 src/String/Alias/NonEmptyStr.php create mode 100755 src/String/DB/StringVarChar255.php rename src/String/{NonEmptyStr.php => StringNonEmpty.php} (94%) rename src/String/{StringBasic.php => StringStandard.php} (91%) rename tests/Unit/{Code/Float/NonNegativeFloatTypeTest.php => Float/FloatNonNegativeTypeTest.php} (57%) create mode 100755 tests/Unit/String/DB/StringVarChar255Test.php rename tests/Unit/String/{NonEmptyStrTypeTest.php => StringNonEmptyTypeTest.php} (73%) diff --git a/docs/USAGE.md b/docs/USAGE.md index 6935861..9daa5ce 100755 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -41,10 +41,10 @@ Static usage examples --------------------- ```php -use PhpTypedValues\DateTime\DateTimeAtom;use PhpTypedValues\DateTime\Timestamp\TimestampSeconds;use PhpTypedValues\Float\FloatBasic;use PhpTypedValues\Float\NonNegativeFloat;use PhpTypedValues\Integer\IntegerStandart;use PhpTypedValues\Integer\IntegerNonNegative;use PhpTypedValues\Integer\IntegerPositive;use PhpTypedValues\Integer\IntegerWeekDay;use PhpTypedValues\String\NonEmptyStr;use PhpTypedValues\String\StringBasic; +use PhpTypedValues\DateTime\DateTimeAtom;use PhpTypedValues\DateTime\Timestamp\TimestampSeconds;use PhpTypedValues\Float\FloatStandard;use PhpTypedValues\Float\FloatNonNegative;use PhpTypedValues\Integer\IntegerStandard;use PhpTypedValues\Integer\IntegerNonNegative;use PhpTypedValues\Integer\IntegerPositive;use PhpTypedValues\Integer\IntegerWeekDay;use PhpTypedValues\String\StringNonEmpty;use PhpTypedValues\String\StringStandard; // Integers -$any = IntegerStandart::fromInt(-10); +$any = IntegerStandard::fromInt(-10); $pos = IntegerPositive::fromInt(1); $nn = IntegerNonNegative::fromInt(0); $wd = IntegerWeekDay::fromInt(7); // 1..7 @@ -54,12 +54,12 @@ $posFromString = IntegerPositive::fromString('123'); $wdFromString = IntegerWeekDay::fromString('5'); // Strings -$greeting = StringBasic::fromString('hello'); -$name = NonEmptyStr::fromString('Alice'); +$greeting = StringStandard::fromString('hello'); +$name = StringNonEmpty::fromString('Alice'); // Floats -$price = FloatBasic::fromString('19.99'); -$ratio = NonNegativeFloat::fromFloat(0.5); // >= 0 +$price = FloatStandard::fromString('19.99'); +$ratio = FloatNonNegative::fromFloat(0.5); // >= 0 // DateTime (RFC 3339 / ATOM) $dt = DateTimeAtom::fromString('2025-01-02T03:04:05+00:00'); @@ -83,8 +83,8 @@ Invalid input throws an exception with a helpful message. ```php use PhpTypedValues\Integer\IntegerPositive; use PhpTypedValues\Integer\IntegerWeekDay; -use PhpTypedValues\String\NonEmptyStr; -use PhpTypedValues\Float\NonNegativeFloat; +use PhpTypedValues\String\StringNonEmpty; +use PhpTypedValues\Float\FloatNonNegative; use PhpTypedValues\DateTime\DateTimeAtom; IntegerPositive::fromInt(0); // throws: must be > 0 @@ -92,9 +92,9 @@ IntegerPositive::fromString('12.3'); // throws: String has no valid integer IntegerWeekDay::fromInt(0); // throws: Value must be between 1 and 7 -NonEmptyStr::fromString(''); // throws: Value must be a non-empty string +StringNonEmpty::fromString(''); // throws: Value must be a non-empty string -NonNegativeFloat::fromString('abc'); // throws: String has no valid float +FloatNonNegative::fromString('abc'); // throws: String has no valid float DateTimeAtom::fromString('not-a-date'); // throws: String has no valid datetime ``` @@ -191,19 +191,19 @@ declare(strict_types=1); namespace App\Domain; use PhpTypedValues\Integer\IntegerPositive; -use PhpTypedValues\String\NonEmptyStr; -use PhpTypedValues\Float\NonNegativeFloat; +use PhpTypedValues\String\StringNonEmpty; +use PhpTypedValues\Float\FloatNonNegative; use PhpTypedValues\DateTime\DateTimeAtom; final class Profile { public function __construct( public readonly IntegerPositive $id, - public readonly NonEmptyStr $firstName, - public readonly NonEmptyStr $lastName, - public readonly ?NonEmptyStr $middleName, // nullable field + public readonly StringNonEmpty $firstName, + public readonly StringNonEmpty $lastName, + public readonly ?StringNonEmpty $middleName, // nullable field public readonly ?DateTimeAtom $birthDate, // nullable field - public readonly ?NonNegativeFloat $heightM // nullable field + public readonly ?FloatNonNegative $heightM // nullable field ) {} // Convenience named constructor that accepts raw scalars and builds primitives internally @@ -217,11 +217,11 @@ final class Profile ): self { return new self( IntegerPositive::fromInt($id), - NonEmptyStr::fromString($firstName), - NonEmptyStr::fromString($lastName), - $middleName !== null ? NonEmptyStr::fromString($middleName) : null, + StringNonEmpty::fromString($firstName), + StringNonEmpty::fromString($lastName), + $middleName !== null ? StringNonEmpty::fromString($middleName) : null, $birthDateAtom !== null ? DateTimeAtom::fromString($birthDateAtom) : null, - $heightM !== null ? NonNegativeFloat::fromString((string)$heightM) : null, + $heightM !== null ? FloatNonNegative::fromString((string)$heightM) : null, ); } } @@ -238,9 +238,9 @@ $p1 = Profile::fromScalars( $p2 = new Profile( id: IntegerPositive::fromInt(202), - firstName: NonEmptyStr::fromString('Bob'), - lastName: NonEmptyStr::fromString('Johnson'), - middleName: NonEmptyStr::fromString('A.'), + firstName: StringNonEmpty::fromString('Bob'), + lastName: StringNonEmpty::fromString('Johnson'), + middleName: StringNonEmpty::fromString('A.'), birthDate: null, heightM: null, ); diff --git a/src/Float/Alias/NonNegativeFloat.php b/src/Float/Alias/NonNegativeFloat.php new file mode 100755 index 0000000..e8d63f5 --- /dev/null +++ b/src/Float/Alias/NonNegativeFloat.php @@ -0,0 +1,14 @@ + 255) { + throw new StringTypeException('String is too long, max 255 chars allowed'); + } + + $this->value = $value; + } + + /** + * @throws StringTypeException + */ + public static function fromString(string $value): static + { + return new static($value); + } + + public function value(): string + { + return $this->value; + } +} diff --git a/src/String/NonEmptyStr.php b/src/String/StringNonEmpty.php similarity index 94% rename from src/String/NonEmptyStr.php rename to src/String/StringNonEmpty.php index cf98b5b..9268a1c 100755 --- a/src/String/NonEmptyStr.php +++ b/src/String/StringNonEmpty.php @@ -12,7 +12,7 @@ /** * @psalm-immutable */ -readonly class NonEmptyStr extends StrType +readonly class StringNonEmpty extends StrType { /** @var non-empty-string */ protected string $value; diff --git a/src/String/StringBasic.php b/src/String/StringStandard.php similarity index 91% rename from src/String/StringBasic.php rename to src/String/StringStandard.php index 760adb8..517d05c 100755 --- a/src/String/StringBasic.php +++ b/src/String/StringStandard.php @@ -11,7 +11,7 @@ * * @psalm-immutable */ -readonly class StringBasic extends StrType +readonly class StringStandard extends StrType { protected string $value; diff --git a/src/psalmTest.php b/src/psalmTest.php index 7122b80..f3edc9e 100755 --- a/src/psalmTest.php +++ b/src/psalmTest.php @@ -12,49 +12,53 @@ use PhpTypedValues\DateTime\DateTimeRFC3339; use PhpTypedValues\DateTime\Timestamp\TimestampMilliseconds; use PhpTypedValues\DateTime\Timestamp\TimestampSeconds; -use PhpTypedValues\Float\FloatBasic; -use PhpTypedValues\Float\NonNegativeFloat; +use PhpTypedValues\Float\Alias\NonNegativeFloat; +use PhpTypedValues\Float\FloatNonNegative; +use PhpTypedValues\Float\FloatStandard; use PhpTypedValues\Integer\Alias\Id; use PhpTypedValues\Integer\Alias\NonNegativeInt; use PhpTypedValues\Integer\Alias\PositiveInt; use PhpTypedValues\Integer\IntegerNonNegative; use PhpTypedValues\Integer\IntegerPositive; -use PhpTypedValues\Integer\IntegerStandart; +use PhpTypedValues\Integer\IntegerStandard; use PhpTypedValues\Integer\IntegerWeekDay; -use PhpTypedValues\String\NonEmptyStr; -use PhpTypedValues\String\StringBasic; +use PhpTypedValues\String\Alias\NonEmptyStr; +use PhpTypedValues\String\StringNonEmpty; +use PhpTypedValues\String\StringStandard; /** * Integer. */ -testInteger(IntegerStandart::fromInt(10)->value()); +testInteger(IntegerStandard::fromInt(10)->value()); testPositiveInt(IntegerPositive::fromInt(10)->value()); testNonNegativeInt(IntegerNonNegative::fromInt(10)->value()); testWeekDayInt(IntegerWeekDay::fromInt(7)->value()); echo NonNegativeInt::fromString('10')->toString() . \PHP_EOL; echo PositiveInt::fromString('10')->toString() . \PHP_EOL; -echo IntegerStandart::fromString('10')->toString() . \PHP_EOL; +echo IntegerStandard::fromString('10')->toString() . \PHP_EOL; echo Id::fromString('10')->toString() . \PHP_EOL; /** * String. */ -testString(StringBasic::fromString('hi')->value()); -testNonEmptyString(NonEmptyStr::fromString('hi')->value()); +testString(StringStandard::fromString('hi')->value()); +testNonEmptyString(StringNonEmpty::fromString('hi')->value()); -echo StringBasic::fromString('hi')->toString() . \PHP_EOL; +echo StringStandard::fromString('hi')->toString() . \PHP_EOL; +echo NonEmptyStr::fromString('hi')->toString() . \PHP_EOL; /** * Float. */ -testFloat(FloatBasic::fromFloat(3.14)->value()); +testFloat(FloatStandard::fromFloat(3.14)->value()); -echo FloatBasic::fromString('2.71828')->toString() . \PHP_EOL; +echo FloatStandard::fromString('2.71828')->toString() . \PHP_EOL; +echo NonNegativeFloat::fromString('2.71828')->toString() . \PHP_EOL; // PositiveFloat usage -testPositiveFloat(NonNegativeFloat::fromFloat(0.5)->value()); -echo NonNegativeFloat::fromString('3.14159')->toString() . \PHP_EOL; +testPositiveFloat(FloatNonNegative::fromFloat(0.5)->value()); +echo FloatNonNegative::fromString('3.14159')->toString() . \PHP_EOL; /** * DateTime. diff --git a/tests/Unit/Code/Float/FloatTypeTest.php b/tests/Unit/Code/Float/FloatTypeTest.php index 237dc54..45fbd3b 100755 --- a/tests/Unit/Code/Float/FloatTypeTest.php +++ b/tests/Unit/Code/Float/FloatTypeTest.php @@ -3,29 +3,29 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\FloatTypeException; -use PhpTypedValues\Float\FloatBasic; +use PhpTypedValues\Float\FloatStandard; it('fromFloat returns exact value and toString matches', function (): void { - $f1 = FloatBasic::fromFloat(-10.5); + $f1 = FloatStandard::fromFloat(-10.5); expect($f1->value())->toBe(-10.5) ->and($f1->toString())->toBe('-10.5'); - $f2 = FloatBasic::fromFloat(0.0); + $f2 = FloatStandard::fromFloat(0.0); expect($f2->value())->toBe(0.0) ->and($f2->toString())->toBe('0'); }); it('fromString parses valid float strings including negatives, decimals, and scientific', function (): void { - expect(FloatBasic::fromString('-15.25')->value())->toBe(-15.25) - ->and(FloatBasic::fromString('0007.5')->value())->toBe(7.5) - ->and(FloatBasic::fromString('+5.0')->value())->toBe(5.0) - ->and(FloatBasic::fromString('1e3')->value())->toBe(1000.0) - ->and(FloatBasic::fromString('42')->toString())->toBe('42'); + expect(FloatStandard::fromString('-15.25')->value())->toBe(-15.25) + ->and(FloatStandard::fromString('0007.5')->value())->toBe(7.5) + ->and(FloatStandard::fromString('+5.0')->value())->toBe(5.0) + ->and(FloatStandard::fromString('1e3')->value())->toBe(1000.0) + ->and(FloatStandard::fromString('42')->toString())->toBe('42'); }); it('fromString rejects non-numeric strings', function (): void { $invalid = ['5a', 'a5', '', 'abc', '--5', '5,5']; foreach ($invalid as $str) { - expect(fn() => FloatBasic::fromString($str))->toThrow(FloatTypeException::class); + expect(fn() => FloatStandard::fromString($str))->toThrow(FloatTypeException::class); } }); diff --git a/tests/Unit/Code/Integer/IntTypeTest.php b/tests/Unit/Code/Integer/IntTypeTest.php index 60e54e1..f11fea5 100755 --- a/tests/Unit/Code/Integer/IntTypeTest.php +++ b/tests/Unit/Code/Integer/IntTypeTest.php @@ -3,27 +3,27 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\IntegerTypeException; -use PhpTypedValues\Integer\IntegerStandart; +use PhpTypedValues\Integer\IntegerStandard; it('fromInt returns exact value and toString matches', function (): void { - $i1 = IntegerStandart::fromInt(-10); + $i1 = IntegerStandard::fromInt(-10); expect($i1->value())->toBe(-10) ->and($i1->toString())->toBe('-10'); - $i2 = IntegerStandart::fromInt(0); + $i2 = IntegerStandard::fromInt(0); expect($i2->value())->toBe(0) ->and($i2->toString())->toBe('0'); }); it('fromString parses valid integer strings including negatives and leading zeros', function (): void { - expect(IntegerStandart::fromString('-15')->value())->toBe(-15) - ->and(IntegerStandart::fromString('0')->toString())->toBe('0') - ->and(IntegerStandart::fromString('42')->toString())->toBe('42'); + expect(IntegerStandard::fromString('-15')->value())->toBe(-15) + ->and(IntegerStandard::fromString('0')->toString())->toBe('0') + ->and(IntegerStandard::fromString('42')->toString())->toBe('42'); }); it('fromString rejects non-integer strings', function (): void { $invalid = ['5a', 'a5', '', 'abc', ' 5', '5 ', '+5', '05', '--5', '3.14']; foreach ($invalid as $str) { - expect(fn() => IntegerStandart::fromString($str))->toThrow(IntegerTypeException::class); + expect(fn() => IntegerStandard::fromString($str))->toThrow(IntegerTypeException::class); } }); diff --git a/tests/Unit/Code/String/StrTypeTest.php b/tests/Unit/Code/String/StrTypeTest.php index 499b240..fbd5a5f 100755 --- a/tests/Unit/Code/String/StrTypeTest.php +++ b/tests/Unit/Code/String/StrTypeTest.php @@ -2,24 +2,24 @@ declare(strict_types=1); -use PhpTypedValues\String\StringBasic; +use PhpTypedValues\String\StringStandard; it('fromString returns exact value and toString matches', function (): void { - $s1 = StringBasic::fromString('hello'); + $s1 = StringStandard::fromString('hello'); expect($s1->value())->toBe('hello') ->and($s1->toString())->toBe('hello'); - $s2 = StringBasic::fromString(''); + $s2 = StringStandard::fromString(''); expect($s2->value())->toBe('') ->and($s2->toString())->toBe(''); }); it('handles unicode and whitespace transparently', function (): void { - $unicode = StringBasic::fromString('Привет 🌟'); + $unicode = StringStandard::fromString('Привет 🌟'); expect($unicode->value())->toBe('Привет 🌟') ->and($unicode->toString())->toBe('Привет 🌟'); - $ws = StringBasic::fromString(' spaced '); + $ws = StringStandard::fromString(' spaced '); expect($ws->value())->toBe(' spaced ') ->and($ws->toString())->toBe(' spaced '); }); diff --git a/tests/Unit/Code/Float/NonNegativeFloatTypeTest.php b/tests/Unit/Float/FloatNonNegativeTypeTest.php similarity index 57% rename from tests/Unit/Code/Float/NonNegativeFloatTypeTest.php rename to tests/Unit/Float/FloatNonNegativeTypeTest.php index 1a1303c..c7b6efe 100755 --- a/tests/Unit/Code/Float/NonNegativeFloatTypeTest.php +++ b/tests/Unit/Float/FloatNonNegativeTypeTest.php @@ -3,43 +3,43 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\FloatTypeException; -use PhpTypedValues\Float\NonNegativeFloat; +use PhpTypedValues\Float\FloatNonNegative; it('accepts non-negative floats via fromFloat and toString matches', function (): void { - $f0 = NonNegativeFloat::fromFloat(0.0); + $f0 = FloatNonNegative::fromFloat(0.0); expect($f0->value())->toBe(0.0) ->and($f0->toString())->toBe('0'); - $f1 = NonNegativeFloat::fromFloat(1.5); + $f1 = FloatNonNegative::fromFloat(1.5); expect($f1->value())->toBe(1.5) ->and($f1->toString())->toBe('1.5'); }); it('parses non-negative numeric strings via fromString', function (): void { - expect(NonNegativeFloat::fromString('0')->value())->toBe(0.0) - ->and(NonNegativeFloat::fromString('0.0')->value())->toBe(0.0) - ->and(NonNegativeFloat::fromString('3.14')->value())->toBe(3.14) - ->and(NonNegativeFloat::fromString('1e2')->value())->toBe(100.0) - ->and(NonNegativeFloat::fromString('42')->toString())->toBe('42'); + expect(FloatNonNegative::fromString('0')->value())->toBe(0.0) + ->and(FloatNonNegative::fromString('0.0')->value())->toBe(0.0) + ->and(FloatNonNegative::fromString('3.14')->value())->toBe(3.14) + ->and(FloatNonNegative::fromString('1e2')->value())->toBe(100.0) + ->and(FloatNonNegative::fromString('42')->toString())->toBe('42'); }); it('rejects negative values', function (): void { - expect(fn() => new NonNegativeFloat(-0.001)) + expect(fn() => new FloatNonNegative(-0.001)) ->toThrow(FloatTypeException::class); - expect(fn() => NonNegativeFloat::fromFloat(-0.001)) + expect(fn() => FloatNonNegative::fromFloat(-0.001)) ->toThrow(FloatTypeException::class); }); it('rejects non-numeric or negative strings', function (): void { // Non-numeric foreach (['', 'abc', '5,5'] as $str) { - expect(fn() => NonNegativeFloat::fromString($str)) + expect(fn() => FloatNonNegative::fromString($str)) ->toThrow(FloatTypeException::class); } // Numeric but negative foreach (['-1', '-0.1'] as $str) { - expect(fn() => NonNegativeFloat::fromString($str)) + expect(fn() => FloatNonNegative::fromString($str)) ->toThrow(FloatTypeException::class); } }); diff --git a/tests/Unit/Integer/IntegerStandartTypeTest.php b/tests/Unit/Integer/IntegerStandartTypeTest.php index 8c777c0..da94c1f 100755 --- a/tests/Unit/Integer/IntegerStandartTypeTest.php +++ b/tests/Unit/Integer/IntegerStandartTypeTest.php @@ -3,29 +3,29 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\IntegerTypeException; -use PhpTypedValues\Integer\IntegerStandart; +use PhpTypedValues\Integer\IntegerStandard; it('creates Integer from int', function (): void { - expect(IntegerStandart::fromInt(5)->value())->toBe(5); + expect(IntegerStandard::fromInt(5)->value())->toBe(5); }); it('creates Integer from string', function (): void { - expect(IntegerStandart::fromString('5')->value())->toBe(5); + expect(IntegerStandard::fromString('5')->value())->toBe(5); }); it('fails on "integer-ish" float string', function (): void { - expect(fn() => IntegerStandart::fromString('5.'))->toThrow(IntegerTypeException::class); + expect(fn() => IntegerStandard::fromString('5.'))->toThrow(IntegerTypeException::class); }); it('fails on float string', function (): void { - expect(fn() => IntegerStandart::fromString('5.5'))->toThrow(IntegerTypeException::class); + expect(fn() => IntegerStandard::fromString('5.5'))->toThrow(IntegerTypeException::class); }); it('fails on type mismatch', function (): void { expect(function () { try { // invalid integer string (contains decimal point) - IntegerStandart::fromInt('34.66'); + IntegerStandard::fromInt('34.66'); } catch (Throwable $e) { throw new IntegerTypeException('Failed to create Integer from string', previous: $e); } diff --git a/tests/Unit/String/DB/StringVarChar255Test.php b/tests/Unit/String/DB/StringVarChar255Test.php new file mode 100755 index 0000000..489f15b --- /dev/null +++ b/tests/Unit/String/DB/StringVarChar255Test.php @@ -0,0 +1,38 @@ +value())->toBe('') + ->and($s->toString())->toBe(''); +}); + +it('accepts 255 ASCII characters (boundary) and preserves value', function (): void { + $str = str_repeat('a', 255); + $s = StringVarChar255::fromString($str); + expect($s->value())->toBe($str) + ->and($s->toString())->toBe($str); +}); + +it('throws on 256 ASCII characters (above boundary)', function (): void { + $str = str_repeat('b', 256); + expect(fn() => new StringVarChar255($str)) + ->toThrow(StringTypeException::class, 'String is too long, max 255 chars allowed'); +}); + +it('accepts 255 multibyte characters (emoji) counted by mb_strlen', function (): void { + $str = str_repeat('🙂', 255); + $s = new StringVarChar255($str); + expect($s->value())->toBe($str) + ->and($s->toString())->toBe($str); +}); + +it('throws on 256 multibyte characters (emoji)', function (): void { + $str = str_repeat('🙂', 256); + expect(fn() => StringVarChar255::fromString($str)) + ->toThrow(StringTypeException::class, 'String is too long, max 255 chars allowed'); +}); diff --git a/tests/Unit/String/NonEmptyStrTypeTest.php b/tests/Unit/String/StringNonEmptyTypeTest.php similarity index 73% rename from tests/Unit/String/NonEmptyStrTypeTest.php rename to tests/Unit/String/StringNonEmptyTypeTest.php index 2c60cfa..f96b878 100755 --- a/tests/Unit/String/NonEmptyStrTypeTest.php +++ b/tests/Unit/String/StringNonEmptyTypeTest.php @@ -3,27 +3,27 @@ declare(strict_types=1); use PhpTypedValues\Code\Exception\StringTypeException; -use PhpTypedValues\String\NonEmptyStr; +use PhpTypedValues\String\StringNonEmpty; it('constructs and preserves non-empty string', function (): void { - $s = new NonEmptyStr('hello'); + $s = new StringNonEmpty('hello'); expect($s->value())->toBe('hello') ->and($s->toString())->toBe('hello'); }); it('allows whitespace and unicode as non-empty', function (): void { - $w = new NonEmptyStr(' '); - $u = NonEmptyStr::fromString('🙂'); + $w = new StringNonEmpty(' '); + $u = StringNonEmpty::fromString('🙂'); expect($w->value())->toBe(' ') ->and($u->toString())->toBe('🙂'); }); it('throws on empty string via constructor', function (): void { - expect(fn() => new NonEmptyStr('')) + expect(fn() => new StringNonEmpty('')) ->toThrow(StringTypeException::class, 'Expected non-empty string, got ""'); }); it('throws on empty string via fromString', function (): void { - expect(fn() => NonEmptyStr::fromString('')) + expect(fn() => StringNonEmpty::fromString('')) ->toThrow(StringTypeException::class, 'Expected non-empty string, got ""'); }); From 8c3637597e5d1dbe66f12ad96bccbbca777eb129 Mon Sep 17 00:00:00 2001 From: GeorgII Date: Wed, 26 Nov 2025 21:55:34 +0100 Subject: [PATCH 4/4] Add `StringUuidV4` type with strict UUID v4 validation and comprehensive tests - Introduced `StringUuidV4` class to validate and normalize UUID v4 values (RFC 4122). - Added detailed error messages for invalid UUID formats, incorrect versions, empty values, or invalid variants. - Implemented comprehensive unit tests covering valid UUID preservation, normalization, and various error scenarios. --- src/String/StringUuidV4.php | 59 ++++++++++++++++++++++++++ tests/Unit/String/StringUuidV4Test.php | 45 ++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100755 src/String/StringUuidV4.php create mode 100755 tests/Unit/String/StringUuidV4Test.php diff --git a/src/String/StringUuidV4.php b/src/String/StringUuidV4.php new file mode 100755 index 0000000..61d005a --- /dev/null +++ b/src/String/StringUuidV4.php @@ -0,0 +1,59 @@ +value = $normalized; + } + + /** + * @throws StringTypeException + */ + public static function fromString(string $value): static + { + return new static($value); + } + + /** @return non-empty-string */ + public function value(): string + { + return $this->value; + } +} diff --git a/tests/Unit/String/StringUuidV4Test.php b/tests/Unit/String/StringUuidV4Test.php new file mode 100755 index 0000000..f326a3f --- /dev/null +++ b/tests/Unit/String/StringUuidV4Test.php @@ -0,0 +1,45 @@ +value())->toBe($uuid) + ->and($s->toString())->toBe($uuid); +}); + +it('normalizes uppercase input to lowercase while preserving the UUID semantics', function (): void { + $upper = '550E8400-E29B-41D4-A716-446655440000'; + $s = StringUuidV4::fromString($upper); + + expect($s->value())->toBe('550e8400-e29b-41d4-a716-446655440000') + ->and($s->toString())->toBe('550e8400-e29b-41d4-a716-446655440000'); +}); + +it('throws on empty string', function (): void { + expect(fn() => new StringUuidV4('')) + ->toThrow(StringTypeException::class, 'Expected non-empty UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got ""'); +}); + +it('throws when UUID version is not 4 (e.g., version 1)', function (): void { + $v1 = '550e8400-e29b-11d4-a716-446655440000'; + expect(fn() => StringUuidV4::fromString($v1)) + ->toThrow(StringTypeException::class, 'Expected UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got "' . $v1 . '"'); +}); + +it('throws when UUID variant nibble is invalid (must be 8,9,a,b)', function (): void { + $badVariant = '550e8400-e29b-41d4-7716-446655440000'; + expect(fn() => new StringUuidV4($badVariant)) + ->toThrow(StringTypeException::class, 'Expected UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got "' . $badVariant . '"'); +}); + +it('throws on invalid characters or format (non-hex character)', function (): void { + $badChar = '550e8400-e29b-41d4-a716-44665544000g'; + expect(fn() => StringUuidV4::fromString($badChar)) + ->toThrow(StringTypeException::class, 'Expected UUID v4 (xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx), got "' . $badChar . '"'); +});