diff --git a/src/protocol/v6/structures/TypeMarker.php b/src/protocol/v6/structures/TypeMarker.php new file mode 100644 index 0000000..1d22b5b --- /dev/null +++ b/src/protocol/v6/structures/TypeMarker.php @@ -0,0 +1,20 @@ +type_marker, (string)$this->data]); } - private static array $formats = ['s', 'l', 'q']; + private static array $endiannessFormats = ['s', 'l', 'q']; /** * Encode array as vector structure + * This is a helper method to create Vector structure from array of numbers * @param int[]|float[] $data + * @param TypeMarker|null $type Optional type to force specific data type .. null = auto decide * @return self * @throws \InvalidArgumentException */ - public static function encode(array $data): self + public static function encode(array $data, ?TypeMarker $type = null): self { - if (count($data) === 0) { - throw new \InvalidArgumentException('Vector cannot be empty'); + $anyFloat = false; + foreach ($data as $entry) { + if (!is_int($entry) && !is_float($entry)) { + throw new \InvalidArgumentException('Vector can only contain numeric values'); + } + if (!$anyFloat && is_float($entry)) { + $anyFloat = true; + } } - if (count($data) > 4096) { - throw new \InvalidArgumentException('Vector cannot have more than 4096 elements'); + + if ($type === null) { + $type = self::detectTypeMarker($anyFloat, count($data) ? min($data) : 0, count($data) ? max($data) : 0); } - $anyFloat = in_array(true, array_map('is_float', $data)); - $minValue = min($data); - $maxValue = max($data); - $marker = 0; $packFormat = ''; - - if ($anyFloat) { - if ($minValue >= 1.4e-45 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32) - $marker = 0xC6; + switch ($type) { + case TypeMarker::FLOAT_32: $packFormat = 'G'; - } else { // Double precision float (FLOAT_64) - $marker = 0xC1; + break; + case TypeMarker::FLOAT_64: $packFormat = 'E'; - } - } else { - if ($minValue >= -128 && $maxValue <= 127) { // INT_8 - $marker = 0xC8; + break; + case TypeMarker::INT_8: $packFormat = 'c'; - } elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16 - $marker = 0xC9; + break; + case TypeMarker::INT_16: $packFormat = 's'; - } elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32 - $marker = 0xCA; + break; + case TypeMarker::INT_32: $packFormat = 'l'; - } else { // INT_64 - $marker = 0xCB; + break; + case TypeMarker::INT_64: $packFormat = 'q'; - } - } - - if ($marker === 0) { - throw new \InvalidArgumentException('Unsupported data type for vector'); + break; } // Pack the data $packed = []; $littleEndian = unpack('S', "\x01\x00")[1] === 1; foreach ($data as $entry) { - $value = pack($packFormat, $entry); - $packed[] = in_array($packFormat, self::$formats) && $littleEndian ? strrev($value) : $value; + $value = pack($packFormat, $anyFloat ? (float)$entry : (int)$entry); + $packed[] = in_array($packFormat, self::$endiannessFormats) && $littleEndian ? strrev($value) : $value; } - return new self(new Bytes([chr($marker)]), new Bytes($packed)); + return new self(new Bytes([chr($type->value)]), new Bytes($packed)); + } + + private static function detectTypeMarker(bool $anyFloat, int|float $minValue, int|float $maxValue): TypeMarker + { + if ($anyFloat) { + if ($minValue >= -3.4028235e+38 && $maxValue <= 3.4028235e+38) { // Single precision float (FLOAT_32) + return TypeMarker::FLOAT_32; + } else { // Double precision float (FLOAT_64) + return TypeMarker::FLOAT_64; + } + } else { + if ($minValue >= -128 && $maxValue <= 127) { // INT_8 + return TypeMarker::INT_8; + } elseif ($minValue >= -32768 && $maxValue <= 32767) { // INT_16 + return TypeMarker::INT_16; + } elseif ($minValue >= -2147483648 && $maxValue <= 2147483647) { // INT_32 + return TypeMarker::INT_32; + } else { // INT_64 + return TypeMarker::INT_64; + } + } } /** - * Decode vector structure .. returns binary $this->data as array + * Decode vector structure .. returns binary $this->data as array of numbers * @return int[]|float[] * @throws \InvalidArgumentException */ public function decode(): array { switch (ord($this->type_marker[0])) { - case 0xC8: // INT_8 + case TypeMarker::INT_8->value: // INT_8 $size = 1; $unpackFormat = 'c'; break; - case 0xC9: // INT_16 + case TypeMarker::INT_16->value: // INT_16 $size = 2; $unpackFormat = 's'; break; - case 0xCA: // INT_32 + case TypeMarker::INT_32->value: // INT_32 $size = 4; $unpackFormat = 'l'; break; - case 0xCB: // INT_64 + case TypeMarker::INT_64->value: // INT_64 $size = 8; $unpackFormat = 'q'; break; - case 0xC6: // FLOAT_32 + case TypeMarker::FLOAT_32->value: // FLOAT_32 $size = 4; $unpackFormat = 'G'; break; - case 0xC1: // FLOAT_64 + case TypeMarker::FLOAT_64->value: // FLOAT_64 $size = 8; $unpackFormat = 'E'; break; default: throw new \InvalidArgumentException('Unknown vector type marker: ' . $this->type_marker[0]); } - + $output = []; $littleEndian = unpack('S', "\x01\x00")[1] === 1; - foreach(mb_str_split((string)$this->data, $size, '8bit') as $value) { - $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$formats) && $littleEndian ? strrev($value) : $value)[1]; + foreach (mb_str_split((string)$this->data, $size, '8bit') as $value) { + $output[] = unpack($unpackFormat, in_array($unpackFormat, self::$endiannessFormats) && $littleEndian ? strrev($value) : $value)[1]; } return $output; diff --git a/tests/structures/V6/StructuresTest.php b/tests/structures/V6/StructuresTest.php index f09eb83..0da9557 100644 --- a/tests/structures/V6/StructuresTest.php +++ b/tests/structures/V6/StructuresTest.php @@ -43,13 +43,16 @@ public function testVector(AProtocol $protocol) //unpack $res = iterator_to_array( $protocol - ->run('CYPHER 25 RETURN vector([1.05, 0.123, 5], 3, FLOAT), + ->run( + 'CYPHER 25 RETURN vector([1.05, 0.123, 5], 3, FLOAT), vector([1.05, 0.123, 5], 3, FLOAT32), vector([5, 543, 342765], 3, INTEGER), vector([5, -60, 120], 3, INTEGER8), vector([5, -20000, 30000], 3, INTEGER16), vector([5, -2000000000, 2000000000], 3, INTEGER32)', - [], ['mode' => 'r']) + [], + ['mode' => 'r'] + ) ->pull() ->getResponses(), false @@ -106,8 +109,6 @@ public function testVector(AProtocol $protocol) public function testVectorExceptions() { $this->expectException(\InvalidArgumentException::class); - Vector::encode([]); - $this->expectException(\InvalidArgumentException::class); - Vector::encode(range(1, 5000)); + Vector::encode(['abc', 'def']); } }