Page MenuHomeDevCentral

D2659.id6723.diff
No OneTemporary

D2659.id6723.diff

diff --git a/omnitools/src/Collections/BaseVector.php b/omnitools/src/Collections/BaseVector.php
--- a/omnitools/src/Collections/BaseVector.php
+++ b/omnitools/src/Collections/BaseVector.php
@@ -130,6 +130,27 @@
return $this;
}
+ /**
+ * Replaces a part of the vector by the specified iterable.
+ *
+ * @param int $offset Allow to replace a part inside the vector by an iterable with keys starting at 0, by adding the specified offset.
+ * @param int $len The maximum amount of elements to read. If 0, the read isn't bounded.
+ */
+ public function replace(iterable $iterable, int $offset = 0, int $len = 0) : self {
+ $itemsCount = 0;
+
+ foreach ($iterable as $key => $value) {
+ $this->items[$key + $offset] = $value;
+
+ $itemsCount++;
+ if ($len > 0 && $itemsCount >= $len) {
+ break;
+ }
+ }
+
+ return $this;
+ }
+
/**
* Gets a copy of the internal vector.
*
@@ -194,6 +215,15 @@
);
}
+ public function chunk (int $length): Vector {
+ return new Vector(array_chunk($this->items, $length));
+ }
+
+ public function slice (int $offset, int $length) : self {
+ $slice = array_slice($this->items, $offset, $length);
+ return new static($slice);
+ }
+
public function implode(string $delimiter) : OmniString {
return new OmniString(implode($delimiter, $this->items));
}
diff --git a/omnitools/src/Collections/BitsVector.php b/omnitools/src/Collections/BitsVector.php
new file mode 100644
--- /dev/null
+++ b/omnitools/src/Collections/BitsVector.php
@@ -0,0 +1,230 @@
+<?php
+declare(strict_types=1);
+
+namespace Keruald\OmniTools\Collections;
+
+use Exception;
+use InvalidArgumentException;
+
+class BitsVector extends BaseVector {
+
+ ///
+ /// Constructors
+ ///
+
+ public static function new (int $capacity) : self {
+ return match (true) {
+ $capacity === 0 => new self([]),
+ $capacity < 0 => throw new InvalidArgumentException("Capacity must be a positive number"),
+ default => new self(array_fill(0, $capacity, 0)),
+ };
+ }
+
+ public static function fromInteger (int $n) : self {
+ return self::fromBinaryString(decbin($n));
+ }
+
+ public static function fromBinaryString (string $bits) : self {
+ $vector = ArrayUtilities::toIntegers(str_split($bits));
+ return new self($vector);
+ }
+
+ public static function fromHexString (string $number) : self {
+ $bits = array_map(
+ fn($n) => str_pad(decbin(sscanf(implode($n), "%x")[0]), 8, "0", STR_PAD_LEFT),
+ array_chunk(str_split($number), 2)
+ );
+
+ return self::fromBinaryString(implode($bits));
+ }
+
+ public static function fromDecoratedHexString (string $expression) : self {
+ $number = preg_replace("/[^a-fA-F0-9]/", "", $expression);
+ return self::fromHexString($number);
+ }
+
+ public static function fromString (string $string) : self {
+ return BitsVector::fromHexString(bin2hex($string));
+ }
+
+ /**
+ * @throws Exception if an appropriate source of randomness cannot be found.
+ */
+ public static function random (int $length) : self {
+ if ($length < 0) {
+ throw new InvalidArgumentException("The length must be a positive value");
+ }
+
+ if ($length === 0) {
+ return new self([]);
+ }
+
+ $randomBytes = random_bytes((int)ceil($length / 8));
+
+ return BitsVector::fromString($randomBytes)->truncate($length);
+ }
+
+ ///
+ /// Specialized methods to work with bits vector
+ ///
+
+ public function toBinaryString () : string {
+ return implode($this->items);
+ }
+
+ public function toInteger () : int {
+ if ($this->count() > 63) {
+ throw new InvalidArgumentException("PHP doesn't allow representing integers greater than 64 bits.");
+ }
+
+ return bindec($this->toBinaryString());
+ }
+
+ private static function bitsToHex (array $bits) : string {
+ $number = implode("", $bits);
+ return base_convert($number, 2, 16);
+ }
+
+ public function toHexString () : string {
+ $expectedLen = (int)ceil($this->count() / 4);
+
+ return $this
+ ->chunk(4)
+ ->map(fn($chunk) => self::bitsToHex($chunk))
+ ->implode("")
+ ->pad($expectedLen, "0", STR_PAD_RIGHT)
+ ;
+ }
+
+ public function toBytesArray () : array {
+ if ($this->count() % 8 !== 0) {
+ throw new InvalidArgumentException("This vector can't be represented in bytes: the bits count is not a multiple of 8.");
+ }
+
+ return $this
+ ->chunk(8)
+ ->map(fn ($chunk) => bindec(implode($chunk)))
+ ->toArray();
+ }
+
+ /**
+ * Pad the vector with 0 at the leftmost position ("zerofill")
+ *
+ * @param int $length The expected length of the vector
+ */
+ public function pad (int $length) : self {
+ $currentCount = $this->count();
+
+ if ($currentCount >= $length) {
+ return $this;
+ }
+
+ $bits = array_fill(0, $length - $currentCount, 0);
+ array_push($bits, ...$this->items);
+
+ $this->items = $bits;
+ return $this;
+ }
+
+ public function truncate (int $length) : self {
+ if ($this->count() <= $length) {
+ return $this;
+ }
+
+ $bits = array_slice($this->items, 0, $length);
+
+ $this->items = $bits;
+ return $this;
+ }
+
+ /**
+ * Truncate or pad the array as needed to ensure a specified length.
+ */
+ public function shapeCapacity (int $length) : self {
+ $currentCount = $this->count();
+
+ if ($currentCount == $length) {
+ return $this;
+ }
+
+ if ($currentCount < $length) {
+ return $this->pad($length);
+ }
+
+ return $this->truncate($length);
+ }
+
+ /**
+ * Copy the bits of the specified integer to the bits vector
+ * at the specified offset.
+ *
+ * @param int $integer The integer to copy bits from
+ * @param int $offset The position in the vector where to start to copy
+ * @param int $length The length the bits of the integer occupy in the vector
+ */
+ public function copyInteger (int $integer, int $offset, int $length) : self {
+ $toInsert = self::fromInteger($integer)
+ ->shapeCapacity($length);
+
+ return $this->replace($toInsert, $offset, $length);
+ }
+
+ ///
+ /// Ensure value is always 0 or 1 bit
+ ///
+
+ private static function assertValueIsBit (mixed $value) : void {
+ if ($value !== 0 && $value !== 1) {
+ throw new InvalidArgumentException("Only 0 and 1 are accepted as bit value.");
+ }
+ }
+
+ private static function assertValueIsBitsIterable (iterable $bits) {
+ foreach ($bits as $bit) {
+ self::assertValueIsBit($bit);
+ }
+ }
+
+ ///
+ /// Override of BaseVector
+ /// Ensure value is always 0 or 1 bit
+ ///
+ /// This section is the cost to pay for not having generics
+ ///
+
+ public function __construct (iterable $bits = []) {
+ self::assertValueIsBitsIterable($bits);
+ parent::__construct($bits);
+ }
+
+ public function set (int $key, mixed $value) : static {
+ self::assertValueIsBit($value);
+ return parent::set($key, $value);
+ }
+
+ public function contains (mixed $value) : bool {
+ self::assertValueIsBit($value);
+ return parent::contains($value);
+ }
+
+ public function push (mixed $item) : self {
+ self::assertValueIsBit($item);
+ return parent::push($item);
+ }
+
+ public function append (iterable $iterable) : self {
+ self::assertValueIsBitsIterable($iterable);
+ return parent::append($iterable);
+ }
+
+ public function update (iterable $iterable) : self {
+ self::assertValueIsBitsIterable($iterable);
+ return parent::update($iterable);
+ }
+
+ public function offsetSet (mixed $offset, mixed $value) : void {
+ self::assertValueIsBit($value);
+ parent::offsetSet($offset, $value);
+ }
+
+}
diff --git a/omnitools/tests/Collections/BitsVectorTest.php b/omnitools/tests/Collections/BitsVectorTest.php
new file mode 100644
--- /dev/null
+++ b/omnitools/tests/Collections/BitsVectorTest.php
@@ -0,0 +1,233 @@
+<?php
+
+namespace Keruald\OmniTools\Tests\Collections;
+
+use InvalidArgumentException;
+
+use Keruald\OmniTools\Collections\BitsVector;
+use PHPUnit\Framework\TestCase;
+
+class BitsVectorTest extends TestCase {
+
+ ///
+ /// Constructors
+ ///
+
+ public function testConstructorWithInvalidIterable () : void {
+ $this->expectException(InvalidArgumentException::class);
+ new BitsVector([1, 2, 3, 4]);
+ }
+
+ public function testNew () : void {
+ $bits = BitsVector::new(8);
+ $this->assertEquals(8, $bits->count());
+ }
+
+ public function testNewWhenCapacityIsEmpty () : void {
+ $bits = BitsVector::new(0);
+ $this->assertEquals(0, $bits->count());
+ }
+
+ public function testNewWhenCapacityIsNegative () : void {
+ $this->expectException(InvalidArgumentException::class);
+ BitsVector::new(-8);
+ }
+
+ public function testFromInteger () : void {
+ $bits = BitsVector::fromInteger(4);
+ $this->assertEquals([1, 0, 0], $bits->toArray());
+ }
+
+ public function testFromIntegerWhenValueIsNegative () : void {
+ $bits = BitsVector::fromInteger(-1);
+
+ $expected = array_fill(0, 64, 1);
+ $this->assertEquals($expected, $bits->toArray());
+ }
+
+ public function testFromBinString () : void {
+ $bits = BitsVector::fromBinaryString("1001");
+ $this->assertEquals([1, 0, 0, 1], $bits->toArray());
+ }
+
+ public function testFromHexString () : void {
+ $bits = BitsVector::fromHexString("337362ea");
+ $this->assertEquals("337362ea", $bits->toHexString());
+ }
+
+ public function testFromDecoratedHexString () : void {
+ $bits = BitsVector::fromDecoratedHexString("cc4eca23-b825-11ec-ab20-a81e84f35d9c");
+ $this->assertEquals("cc4eca23b82511ecab20a81e84f35d9c", $bits->toHexString());
+ }
+
+ public function testFromString () : void {
+ // Exemple based on pack() documentation
+ $binaryData = pack("nvc*", 0x1234, 0x5678, 65, 66);
+ $bits = BitsVector::fromString($binaryData);
+
+ $this->assertEquals(
+ [0x12, 0x34, 0x78, 0x56, 0x41, 0x42],
+ $bits->toBytesArray(),
+ );
+ }
+
+ public function testBytesArray () : void {
+ $bits = BitsVector::new(16)
+ ->copyInteger(1, 0, 4)
+ ->copyInteger(2, 4, 4)
+ ->copyInteger(3, 8, 4)
+ ->copyInteger(4, 12, 4);
+
+ $this->assertEquals(
+ [0x12, 0x34],
+ $bits->toBytesArray(),
+ );
+ }
+
+ public function provideLengths () : iterable {
+ yield [1];
+ yield [2];
+ yield [8];
+
+ yield [500];
+ yield [5000];
+ yield [0];
+ }
+
+ /**
+ * @dataProvider provideLengths
+ */
+ public function testRandom($length) : void {
+ $bits = BitsVector::random($length);
+ $this->assertEquals($length, $bits->count());
+ }
+
+ public function testRandomWithNegativeLength() : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ BitsVector::random(-1);
+ }
+
+ public function testBytesArrayWithBadLength () : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ $bits = new BitsVector([1, 1, 1]); // 3 bits isn't a byte
+ $bits->toBytesArray();
+ }
+
+ public function testToBinaryString () : void {
+ $bits = new BitsVector([1, 0, 0, 1]);
+ $this->assertEquals("1001", $bits->toBinaryString());
+ }
+
+ public function testToInteger () : void {
+ $bits = new BitsVector([1, 0, 0, 1]);
+ $this->assertEquals(9, $bits->toInteger());
+ }
+
+ public function testToIntegerWhenThereIsTooMuchBits () : void {
+ $this->expectException(InvalidArgumentException::class);
+
+ BitsVector::new(66)->toInteger();
+ }
+
+ public function testPad () : void {
+ $bits = new BitsVector([1, 0, 0, 1]);
+ $bits->pad(8);
+ $this->assertEquals([0, 0, 0, 0, 1, 0, 0, 1], $bits->toArray());
+ }
+
+ public function testPadWithLargeEnoughCount () : void {
+ $bits = new BitsVector([1, 0, 0, 1]);
+ $bits->pad(4);
+ $this->assertEquals([1, 0, 0, 1], $bits->toArray());
+ }
+
+ public function testTruncate () : void {
+ $bits = new BitsVector([1, 0, 0, 1, 0, 0, 0, 0]);
+ $bits->truncate(4);
+
+ $this->assertEquals([1, 0, 0, 1], $bits->toArray());
+ }
+
+ public function testTruncateWithSmallEnoughCount () : void {
+ $bits = new BitsVector([1, 0, 0, 1]);
+ $bits->truncate(4);
+
+ $this->assertEquals([1, 0, 0, 1], $bits->toArray());
+ }
+
+ public function provideShapeArrays () : iterable {
+ yield [[1, 0, 0, 1, 0, 0, 0, 0], 4, [1, 0, 0, 1]];
+ yield [[1, 0, 0, 1], 4, [1, 0, 0, 1]];
+ yield [[1, 0, 0, 1], 3, [1, 0, 0]];
+
+ yield [[1, 0, 0, 1], 0, []];
+ yield [[], 0, []];
+ yield [[], 4, [0, 0, 0, 0]];
+ }
+
+ /**
+ * @dataProvider provideShapeArrays
+ */
+ public function testShapeCapacity (array $initial, int $length, array $final) : void {
+ $bits = new BitsVector($initial);
+ $bits->shapeCapacity($length);
+
+ $this->assertEquals($final, $bits->toArray());
+ }
+
+ public function testCopyInteger() : void {
+ $bits = BitsVector::new(8);
+ $bits->copyInteger(5, 2, 3);
+
+ $this->assertEquals([0, 0, 1, 0, 1, 0, 0, 0], $bits->toArray());
+ }
+
+ ///
+ /// BaseVector overrides
+ ///
+
+ public function testSet () : void {
+ $bits = BitsVector::new(4);
+ $bits->set(2, 1);
+
+ $this->assertEquals([0, 0, 1, 0], $bits->toArray());
+ }
+
+ public function testContains () : void {
+ $bits = BitsVector::new(4);
+
+ $this->assertFalse($bits->contains(1));
+ }
+
+ public function testPush () : void {
+ $bits = BitsVector::new(4);
+ $bits->push(1);
+
+ $this->assertEquals([0, 0, 0, 0, 1], $bits->toArray());
+
+ }
+
+ public function testAppend () : void {
+ $bits = BitsVector::new(4);
+
+ $bits->append([1, 1]);
+ $this->assertEquals([0, 0, 0, 0, 1, 1], $bits->toArray());
+ }
+
+ public function testUpdate () : void {
+ $bits = BitsVector::new(4);
+
+ $bits->update([1, 0, 1, 0]); // 0 already exists, we'll add ONE 1
+ $this->assertEquals([0, 0, 0, 0, 1], $bits->toArray());
+ }
+
+ public function testOffsetSet () : void {
+ $bits = BitsVector::new(4);
+ $bits[2] = 1;
+
+ $this->assertEquals([0, 0, 1, 0], $bits->toArray());
+ }
+
+}
diff --git a/omnitools/tests/Collections/VectorTest.php b/omnitools/tests/Collections/VectorTest.php
--- a/omnitools/tests/Collections/VectorTest.php
+++ b/omnitools/tests/Collections/VectorTest.php
@@ -11,6 +11,10 @@
use IteratorAggregate;
use Traversable;
+/**
+ * @covers \Keruald\OmniTools\Collections\Vector
+ * @covers \Keruald\OmniTools\Collections\BaseVector
+ */
class VectorTest extends TestCase {
private Vector $vector;
@@ -203,6 +207,20 @@
$this->assertEquals([0, 2, 4], array_keys($actual));
}
+ public function testChunk () : void {
+ $vector = new Vector([1, 2, 3, 4, 5, 6]);
+
+ $this->assertEquals(
+ [[1, 2], [3, 4], [5, 6]],
+ $vector->chunk(2)->toArray()
+ );
+ }
+
+ public function testSlice () : void {
+ $actual = $this->vector->slice(2, 3);
+ $this->assertEquals([3, 4, 5], $actual->toArray());
+ }
+
public function testImplode() : void {
$actual = (new Vector(["a", "b", "c"]))
->implode(".")

File Metadata

Mime Type
text/plain
Expires
Tue, Nov 26, 11:20 (21 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2264426
Default Alt Text
D2659.id6723.diff (16 KB)

Event Timeline