diff --git a/CHANGELOG.md b/CHANGELOG.md index aba0574..09fca42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * Introduce a `Builder\FallbackBuilder`, used by `FeatureSet` to help decide whether to return a `Uuid` or `Nonstandard\Uuid` when decoding a UUID string or bytes. +* Introduce `Type\Hexadecimal`, `Type\IntegerValue`, and `Type\Time` for + improved type-safety when dealing with arbitrary string values. +* Introduce `Math\CalculatorInterface` for representing calculators to perform + arithmetic operations on integers. ### Changed diff --git a/src/Math/CalculatorInterface.php b/src/Math/CalculatorInterface.php new file mode 100644 index 0000000..3a64ae2 --- /dev/null +++ b/src/Math/CalculatorInterface.php @@ -0,0 +1,93 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Math; + +use Ramsey\Uuid\Type\Hexadecimal; +use Ramsey\Uuid\Type\IntegerValue; + +/** + * A calculator performs arithmetic operations on numbers + * + * @psalm-immutable + */ +interface CalculatorInterface +{ + /** + * Returns the sum of all the provided parameters + * + * @param IntegerValue $augend The first addend (the integer being added to) + * @param IntegerValue ...$addends The additional integers to a add to the augend + * + * @return IntegerValue The sum of all the parameters + */ + public function add(IntegerValue $augend, IntegerValue ...$addends): IntegerValue; + + /** + * Returns the difference of all the provided parameters + * + * @param IntegerValue $minuend The integer being subtracted from + * @param IntegerValue ...$subtrahends The integers to subtract from the minuend + * + * @return IntegerValue The difference after subtracting all parameters + */ + public function subtract(IntegerValue $minuend, IntegerValue ...$subtrahends): IntegerValue; + + /** + * Returns the product of all the provided parameters + * + * @param IntegerValue $multiplicand The integer to be multiplied + * @param IntegerValue ...$multipliers The factors by which to multiply the multiplicand + * + * @return IntegerValue The product of multiplying all the provided parameters + */ + public function multiply(IntegerValue $multiplicand, IntegerValue ...$multipliers): IntegerValue; + + /** + * Returns the quotient of all the provided parameters divided left-to-right + * + * @param IntegerValue $dividend The integer to be divided + * @param IntegerValue ...$divisors The integers to divide the dividend, in + * the order in which the division operations should take place + * (left-to-right) + * + * @return IntegerValue The quotient of dividing all the provided parameters left-to-right + */ + public function divide(IntegerValue $dividend, IntegerValue ...$divisors): IntegerValue; + + /** + * Converts a value from an arbitrary base to a base-10 integer value + * + * @param string $value The value to convert + * @param int $base The base to convert from (i.e., 2, 16, 32, etc.) + * + * @return IntegerValue The base-10 integer value of the converted value + */ + public function fromBase(string $value, int $base): IntegerValue; + + /** + * Converts a base-10 integer value to an arbitrary base + * + * @param IntegerValue $value The integer value to convert + * @param int $base The base to convert to (i.e., 2, 16, 32, etc.) + * + * @return string The value represented in the specified base + */ + public function toBase(IntegerValue $value, int $base): string; + + /** + * Converts an IntegerValue instance to a Hexadecimal instance + */ + public function toHexadecimal(IntegerValue $value): Hexadecimal; +} diff --git a/src/Type/Hexadecimal.php b/src/Type/Hexadecimal.php new file mode 100644 index 0000000..ee3e952 --- /dev/null +++ b/src/Type/Hexadecimal.php @@ -0,0 +1,66 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Type; + +use Ramsey\Uuid\Exception\InvalidArgumentException; + +use function ctype_xdigit; + +/** + * A value object representing a hexadecimal number + * + * This class exists for type-safety purposes, to ensure that hexadecimal numbers + * returned from ramsey/uuid methods as strings are truly hexadecimal and not some + * other kind of string. + * + * @psalm-immutable + */ +final class Hexadecimal +{ + /** + * @var string + */ + private $value; + + /** + * @param string $value The hexadecimal value to store + */ + public function __construct(string $value) + { + $value = strtolower($value); + + if (strpos($value, '0x') === 0) { + $value = substr($value, 2); + } + + if (!ctype_xdigit($value)) { + throw new InvalidArgumentException( + 'Value must be a hexadecimal number' + ); + } + + $this->value = $value; + } + + public function toString(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/Type/IntegerValue.php b/src/Type/IntegerValue.php new file mode 100644 index 0000000..2d6fb75 --- /dev/null +++ b/src/Type/IntegerValue.php @@ -0,0 +1,78 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Type; + +use Ramsey\Uuid\Exception\InvalidArgumentException; + +use function ctype_digit; + +/** + * A value object representing an integer + * + * This class exists for type-safety purposes, to ensure that integers + * returned from ramsey/uuid methods as strings are truly integers and not some + * other kind of string. + * + * To support large integers beyond PHP_INT_MAX and PHP_INT_MIN on both 64-bit + * and 32-bit systems, we store the integers as strings. + * + * @psalm-immutable + */ +final class IntegerValue +{ + /** + * @var string + */ + private $value; + + /** + * @param mixed $value The integer value to store + */ + public function __construct($value) + { + $value = (string) $value; + $sign = '+'; + + // If the value contains a sign, remove it for ctype_digit() check. + if (strpos($value, '-') === 0 || strpos($value, '+') === 0) { + $sign = substr($value, 0, 1); + $value = substr($value, 1); + } + + if (!ctype_digit($value)) { + throw new InvalidArgumentException( + 'Value must be a signed integer or a string containing only ' + . 'digits 0-9 and, optionally, a sign (+ or -)' + ); + } + + // Add the negative sign back to the value. + if ($sign === '-' && $value !== '0') { + $value = $sign . $value; + } + + $this->value = $value; + } + + public function toString(): string + { + return $this->value; + } + + public function __toString(): string + { + return $this->toString(); + } +} diff --git a/src/Type/Time.php b/src/Type/Time.php new file mode 100644 index 0000000..ba6de41 --- /dev/null +++ b/src/Type/Time.php @@ -0,0 +1,57 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Type; + +/** + * A value object representing a timestamp + * + * This class exists for type-safety purposes, to ensure that timestamps used + * by ramsey/uuid are truly timestamp integers and not some other kind of string + * or integer. + * + * @psalm-immutable + */ +final class Time +{ + /** + * @var IntegerValue + */ + private $seconds; + + /** + * @var IntegerValue + */ + private $microSeconds; + + /** + * @param mixed $seconds + * @param mixed $microSeconds + */ + public function __construct($seconds, $microSeconds = 0) + { + $this->seconds = new IntegerValue($seconds); + $this->microSeconds = new IntegerValue($microSeconds); + } + + public function getSeconds(): IntegerValue + { + return $this->seconds; + } + + public function getMicroSeconds(): IntegerValue + { + return $this->microSeconds; + } +} diff --git a/tests/Type/HexadecimalTest.php b/tests/Type/HexadecimalTest.php new file mode 100644 index 0000000..9281361 --- /dev/null +++ b/tests/Type/HexadecimalTest.php @@ -0,0 +1,74 @@ +assertSame($expected, $hexadecimal->toString()); + $this->assertSame($expected, (string) $hexadecimal); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideHex(): array + { + return [ + [ + 'value' => '0xFFFF', + 'expected' => 'ffff', + ], + [ + 'value' => '0123456789abcdef', + 'expected' => '0123456789abcdef', + ], + [ + 'value' => 'ABCDEF', + 'expected' => 'abcdef', + ], + ]; + } + + /** + * @param int|float|string $value + * + * @dataProvider provideHexBadValues + */ + public function testHexadecimalTypeThrowsExceptionForBadValues($value): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Value must be a hexadecimal number' + ); + + new Hexadecimal($value); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideHexBadValues(): array + { + return [ + ['-123456.789'], + ['123456.789'], + ['foobar'], + ['0xfoobar'], + ]; + } +} diff --git a/tests/Type/IntegerValueTest.php b/tests/Type/IntegerValueTest.php new file mode 100644 index 0000000..fb3c067 --- /dev/null +++ b/tests/Type/IntegerValueTest.php @@ -0,0 +1,166 @@ +assertSame($expected, $integer->toString()); + $this->assertSame($expected, (string) $integer); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideInteger(): array + { + return [ + [ + 'value' => '-11386878954224802805705605120', + 'expected' => '-11386878954224802805705605120', + ], + [ + 'value' => '-9223372036854775808', + 'expected' => '-9223372036854775808', + ], + [ + 'value' => -99986838650880, + 'expected' => '-99986838650880', + ], + [ + 'value' => -4294967296, + 'expected' => '-4294967296', + ], + [ + 'value' => -2147483649, + 'expected' => '-2147483649', + ], + [ + 'value' => -123456.0, + 'expected' => '-123456', + ], + [ + 'value' => -1.00000000000001, + 'expected' => '-1', + ], + [ + 'value' => -1, + 'expected' => '-1', + ], + [ + 'value' => '-1', + 'expected' => '-1', + ], + [ + 'value' => 0, + 'expected' => '0', + ], + [ + 'value' => '0', + 'expected' => '0', + ], + [ + 'value' => -0, + 'expected' => '0', + ], + [ + 'value' => '-0', + 'expected' => '0', + ], + [ + 'value' => '+0', + 'expected' => '0', + ], + [ + 'value' => 1, + 'expected' => '1', + ], + [ + 'value' => '1', + 'expected' => '1', + ], + [ + 'value' => '+1', + 'expected' => '1', + ], + [ + 'value' => 1.00000000000001, + 'expected' => '1', + ], + [ + 'value' => 123456.0, + 'expected' => '123456', + ], + [ + 'value' => 2147483648, + 'expected' => '2147483648', + ], + [ + 'value' => 4294967294, + 'expected' => '4294967294', + ], + [ + 'value' => 99965363767850, + 'expected' => '99965363767850', + ], + [ + 'value' => '9223372036854775808', + 'expected' => '9223372036854775808', + ], + [ + 'value' => '11386878954224802805705605120', + 'expected' => '11386878954224802805705605120', + ], + ]; + } + + /** + * @param int|float|string $value + * + * @dataProvider provideIntegerBadValues + */ + public function testIntegerTypeThrowsExceptionForBadValues($value): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Value must be a signed integer or a string containing only ' + . 'digits 0-9 and, optionally, a sign (+ or -)' + ); + + new IntegerValue($value); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideIntegerBadValues(): array + { + return [ + [-9223372036854775809], // String value is "-9.2233720368548E+18" + [-123456.789], + [-1.0000000000001], + [-0.5], + [0.5], + [1.0000000000001], + [123456.789], + [9223372036854775808], // String value is "9.2233720368548E+18" + ['123abc'], + ['abc123'], + ['foobar'], + ]; + } +} diff --git a/tests/Type/TimeTest.php b/tests/Type/TimeTest.php new file mode 100644 index 0000000..42f691b --- /dev/null +++ b/tests/Type/TimeTest.php @@ -0,0 +1,52 @@ +assertSame((string) $seconds, $time->getSeconds()->toString()); + + $this->assertSame( + (string) $microSeconds ?: '0', + $time->getMicroSeconds()->toString() + ); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideTimeValues(): array + { + return [ + [ + 'seconds' => 103072857659, + 'microSeconds' => null, + ], + [ + 'seconds' => -12219292800, + 'microSeconds' => 1234, + ], + ]; + } +}