* @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved. * * @license BSD 3-Clause License * * This file is part of the php-bitarray package https://github.com/chdemko/php-bitarray */ // Declare chdemko\BitArray namespace namespace chdemko\BitArray; /** * Array of bits * * @package BitArray * * @property-read integer $count The number of bits set to true * @property-read integer $size The number of bits * * @since 1.0.0 */ class BitArray implements \ArrayAccess, \Countable, \IteratorAggregate, \JsonSerializable { /** * @var integer[] Number of bits for each value between 0 and 255 */ private static $count = array( 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8 ); /** * @var integer[] Mask for restricting complements */ private static $restrict = array(255, 1, 3, 7, 15, 31, 63, 127); /** * @var string Underlying data * * @since 1.0.0 */ private $data; /** * @var integer Size of the bit array * * @since 1.0.0 */ private $size; /** * Create a new bit array of the given size * * @param integer $size The BitArray size * @param boolean $default The default value for bits * * @since 1.0.0 */ protected function __construct($size = 0, $default = false) { $this->size = (int) $size; if ($default) { $this->data = str_repeat(chr(255), (int) ceil($this->size / 8)); $this->restrict(); } else { $this->data = str_repeat(chr(0), (int) ceil($this->size / 8)); } } /** * Remove useless bits for simplifying count operation. * * @return BitArray $this for chaining * * @since 1.2.0 */ protected function restrict() { $length = strlen($this->data); if ($length > 0) { $this->data[$length - 1] = chr(ord($this->data[$length - 1]) & self::$restrict[$this->size % 8]); } return $this; } /** * Clone a BitArray * * @return void * * @since 1.0.0 */ public function __clone() { $this->data = str_repeat($this->data, 1); } /** * Convert the object to a string * * @return string String representation of this object * * @since 1.0.0 */ public function __toString() { $string = str_repeat('0', $this->size); for ($offset = 0; $offset < $this->size; $offset++) { if (ord($this->data[(int) ($offset / 8)]) & (1 << $offset % 8)) { $string[$offset] = '1'; } } return $string; } /** * Magic get method * * @param string $property The property * * @throws RuntimeException If the property does not exist * * @return mixed The value associated to the property * * @since 1.0.0 */ public function __get($property) { switch ($property) { case 'size': return $this->size; case 'count': return $this->count(); default: throw new \RuntimeException('Undefined property'); } } /** * Test the existence of an index * * @param integer $offset The offset * * @return boolean The truth value * * @since 1.0.0 */ public function offsetExists($offset): bool { return is_int($offset) && $offset >= 0 && $offset < $this->size; } /** * Get the truth value for an index * * @param integer $offset The offset * * @return boolean The truth value * * @throws OutOfRangeException Argument index must be an positive integer lesser than the size * * @since 1.0.0 */ public function offsetGet($offset): bool { if ($this->offsetExists($offset)) { return (bool) (ord($this->data[(int) ($offset / 8)]) & (1 << $offset % 8)); } else { throw new \OutOfRangeException('Argument offset must be a positive integer lesser than the size'); } } /** * Set the truth value for an index * * @param integer $offset The offset * @param boolean $value The truth value * * @return void * * @throws OutOfRangeException Argument index must be an positive integer lesser than the size * * @since 1.0.0 */ public function offsetSet($offset, $value): void { if ($this->offsetExists($offset)) { $index = (int) ($offset / 8); if ($value) { $this->data[$index] = chr(ord($this->data[$index]) | (1 << $offset % 8)); } else { $this->data[$index] = chr(ord($this->data[$index]) & ~(1 << $offset % 8)); } } else { throw new \OutOfRangeException('Argument index must be a positive integer lesser than the size'); } } /** * Unset the existence of an index * * @param integer $offset The index * * @return void * * @throws RuntimeException Values cannot be unset * * @since 1.0.0 */ public function offsetUnset($offset): void { throw new \RuntimeException('Values cannot be unset'); } /** * Return the number of true bits * * @return integer The number of true bits * * @since 1.0.0 */ public function count(): int { $count = 0; for ($index = 0, $length = strlen($this->data); $index < $length; $index++) { $count += self::$count[ord($this->data[$index])]; } return $count; } /** * Transform the object to an array * * @return array Array of values * * @since 1.1.0 */ public function toArray() { $array = array(); for ($index = 0; $index < $this->size; $index++) { $array[] = (bool) (ord($this->data[(int) ($index / 8)]) & (1 << $index % 8)); } return $array; } /** * Serialize the object * * @return array Array of values * * @since 1.0.0 */ public function jsonSerialize(): array { return $this->toArray(); } /** * Get an iterator * * @return Iterator Iterator * * @since 1.0.0 */ public function getIterator(): Iterator { return new Iterator($this); } /** * Return the size * * @return integer The size * * @since 1.0.0 */ public function size() { return $this->size; } /** * Copy bits directly from a BitArray * * @param BitArray $bits A BitArray to copy * @param int $index Starting index for destination * @param int $offset Starting index for copying * @param int $size Copy size * * @return BitArray This object for chaining * * @throws OutOfRangeException Argument index must be an positive integer lesser than the size * * @since 1.1.0 */ public function directCopy(BitArray $bits, $index = 0, $offset = 0, $size = 0) { if ($offset > $index) { for ($i = 0; $i < $size; $i++) { $this[$i + $index] = $bits[$i + $offset]; } } else { for ($i = $size - 1; $i >= 0; $i--) { $this[$i + $index] = $bits[$i + $offset]; } } return $this; } /** * Copy bits from a BitArray * * * if index is non-negative, the index parameter is used as it is, * keeping its real value between 0 and size-1; * * if index is negative, the index parameter starts from the end, * keeping its real value between 0 and size-1. * * * if offset is non-negative, the offset parameter is used as it is, * keeping its positive value between 0 and size-1; * * if offset is negative, the offset parameter starts from the end, * keeping its real value between 0 and size-1. * * * if size is given and is positive, then the copy will copy size elements. * * if the bits argument is shorter than the size, then only the available elements will be copied. * * if size is given and is negative, then the copy starts from the end. * * if size is omitted, then the copy will have everything from offset up * until the end of the bits argument. * * @param BitArray $bits A BitArray to copy * @param int $index Starting index for destination. * @param int $offset Starting index for copying. * @param mixed $size Copy size. * * @return BitArray This object for chaining * * @since 1.1.0 */ public function copy(BitArray $bits, $index = 0, $offset = 0, $size = null) { $index = $this->getRealOffset($index); $offset = $bits->getRealOffset($offset); $size = $bits->getRealSize($offset, $size); if ($size > $this->size - $index) { $size = $this->size - $index; } return $this->directCopy($bits, $index, $offset, $size); } /** * Get the real offset using a positive or negative offset * * * If offset is non-negative, the offset parameter is used as it is, * keeping its real value between 0 and size-1. * * if offset is negative, the offset parameter starts from the end, * keeping its real value between 0 and size-1. * * @param int $offset The offset * * @return integer The real offset * * @since 1.1.0 */ protected function getRealOffset($offset) { $offset = (int) $offset; if ($offset < 0) { // Start from the end $offset = $this->size + $offset; if ($offset < 0) { $offset = 0; } } elseif ($offset > $this->size) { $offset = $this->size; } return $offset; } /** * Get the real offset using a positive or negative offset * * * if size is given and is positive, then the real size will be between 0 and the current size-1. * * if size is given and is negative, then the real size starts from the end. * * if size is omitted, then the size goes until the end of the BitArray. * * @param int $offset The real offset. * @param mixed $size The size * * @return integer The real size * * @since 1.1.0 */ protected function getRealSize($offset, $size) { if ($size === null) { $size = $this->size - $offset; } else { $size = (int) $size; if ($size < 0) { $size = $this->size + $size - $offset; if ($size < 0) { $size = 0; } } elseif ($size > $this->size - $offset) { $size = $this->size - $offset; } } return $size; } /** * Create a new BitArray from an integer * * @param integer $size Size of the BitArray * @param boolean $default The default value for bits * * @return BitArray A new BitArray * * @since 1.0.0 */ public static function fromInteger($size, $default = false) { return new BitArray($size, (bool) $default); } /** * Create a new BitArray from a sequence of bits. * * @param integer $size Size of the BitArray * @param integer $values The values for the bits * * @return BitArray A new BitArray * * @since 1.2.0 */ public static function fromDecimal($size, $values = 0) { $php_bit_size = PHP_INT_SIZE * 8; $size = min((int) $size, $php_bit_size); $values <<= ($php_bit_size - $size); $bits = new BitArray($size); for ($i = 0; $i < ceil($size / 8); $i++) { $value = ($values & (0xff << ($php_bit_size - 8))) >> ($php_bit_size - 8); $reverse = 0; for ($j = 0; $j < 8; $j++) { if ($value & (1 << $j)) { $reverse |= 1 << (7 - $j); } } $bits->data[$i] = chr($reverse); $values <<= 8; } return $bits; } /** * Create a new BitArray from a traversable * * @param mixed $traversable A traversable and countable * * @return BitArray A new BitArray * * @since 1.0.0 */ public static function fromTraversable($traversable) { $bits = new BitArray(count($traversable)); $offset = 0; $ord = 0; foreach ($traversable as $value) { if ($value) { $ord |= 1 << $offset % 8; } if ($offset % 8 === 7) { $bits->data[(int) ($offset / 8)] = chr($ord); $ord = 0; } $offset++; } if ($offset % 8 !== 0) { $bits->data[(int) ($offset / 8)] = chr($ord); } return $bits; } /** * Create a new BitArray from a bit string * * @param string $string A bit string * * @return BitArray A new BitArray * * @since 1.0.0 */ public static function fromString($string) { $bits = new BitArray(strlen($string)); $ord = 0; for ($offset = 0; $offset < $bits->size; $offset++) { if ($string[$offset] !== '0') { $ord |= 1 << $offset % 8; } if ($offset % 8 === 7) { $bits->data[(int) ($offset / 8)] = chr($ord); $ord = 0; } } if ($offset % 8 !== 0) { $bits->data[(int) ($offset / 8)] = chr($ord); } return $bits; } /** * Create a new BitArray from json * * @param string $json A json encoded value * * @return BitArray A new BitArray * * @since 1.0.0 */ public static function fromJson($json) { return self::fromTraversable(json_decode($json)); } /** * Create a new BitArray using a slice * * * if offset is non-negative, the slice will start at that offset in the bits argument. * * if offset is negative, the slice will start from the end of the bits argument. * * * if size is given and is positive, then the slice will have up to that many elements in it. * * if the bits argument is shorter than the size, then only the available elements will be present. * * if size is given and is negative, * then the slice will stop that many elements from the end of the bits argument. * * if size is omitted, * then the slice will have everything from offset up until the end of the bits argument. * * @param BitArray $bits A BitArray to get the slice * @param int $offset The offset * @param mixed $size The size * * @return BitArray A new BitArray * * @since 1.1.0 */ public static function fromSlice(BitArray $bits, $offset = 0, $size = null) { $offset = $bits->getRealOffset($offset); $size = $bits->getRealSize($offset, $size); $slice = new BitArray($size); return $slice->directCopy($bits, 0, $offset, $size); } /** * Create a new BitArray using the concat operation * * @param BitArray $bits1 A BitArray * @param BitArray $bits2 A BitArray * * @return BitArray A new BitArray * * @since 1.1.0 */ public static function fromConcat(BitArray $bits1, BitArray $bits2) { $size = $bits1->size + $bits2->size; $concat = new BitArray($size); $concat->directCopy($bits1, 0, 0, $bits1->size); $concat->directCopy($bits2, $bits1->size, 0, $bits2->size); return $concat; } /** * Complement the bit array * * @return BitArray This object for chaining * * @since 1.0.0 */ public function applyComplement() { $length = strlen($this->data); for ($index = 0; $index < $length; $index++) { $this->data[$index] = chr(~ ord($this->data[$index])); } return $this->restrict(); } /** * Or with an another bit array * * @param BitArray $bits A bit array * * @return BitArray This object for chaining * * @throws InvalidArgumentException Argument must be of equal size * * @since 1.0.0 */ public function applyOr(BitArray $bits) { if ($this->size == $bits->size) { $length = strlen($this->data); for ($index = 0; $index < $length; $index++) { $this->data[$index] = chr(ord($this->data[$index]) | ord($bits->data[$index])); } return $this; } else { throw new \InvalidArgumentException('Argument must be of equal size'); } } /** * And with an another bit array * * @param BitArray $bits A bit array * * @return BitArray This object for chaining * * @throws InvalidArgumentException Argument must be of equal size * * @since 1.0.0 */ public function applyAnd(BitArray $bits) { if ($this->size == $bits->size) { $length = strlen($this->data); for ($index = 0; $index < $length; $index++) { $this->data[$index] = chr(ord($this->data[$index]) & ord($bits->data[$index])); } return $this; } else { throw new \InvalidArgumentException('Argument must be of equal size'); } } /** * Xor with an another bit array * * @param BitArray $bits A bit array * * @return BitArray This object for chaining * * @throws InvalidArgumentException Argument must be of equal size * * @since 1.0.0 */ public function applyXor(BitArray $bits) { if ($this->size == $bits->size) { $length = strlen($this->data); for ($index = 0; $index < $length; $index++) { $this->data[$index] = chr(ord($this->data[$index]) ^ ord($bits->data[$index])); } return $this; } else { throw new \InvalidArgumentException('Argument must be of equal size'); } } /** * Shift a bit array. * * Negative value means the shifting is done right to left * while positive value means the shifting is done left to right. * * @param int $size Size to shift. * * @param boolean $value Value to shift * * @return BitArray $this for chaining * * @since 1.2.0 */ public function shift($size = 1, $value = false) { $size = (int) $size; if ($size > 0) { $min = min($this->size, $size); for ($i = $this->size - 1; $i >= $min; $i--) { $this[$i] = $this[$i - $min]; } for ($i = 0; $i < $min; $i++) { $this[$i] = $value; } } else { $min = min($this->size, -$size); for ($i = 0; $i < $this->size - $min; $i++) { $this[$i] = $this[$i + $min]; } for ($i = $this->size - $min; $i < $this->size; $i++) { $this[$i] = $value; } } return $this; } }