BitArray::offsetGet()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 6
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * chdemko\BitArray\BitArray class
5
 *
6
 * @author    Christophe Demko <[email protected]>
7
 * @copyright Copyright (C) 2012-2024 Christophe Demko. All rights reserved.
8
 *
9
 * @license BSD 3-Clause License
10
 *
11
 * This file is part of the php-bitarray package https://github.com/chdemko/php-bitarray
12
 */
13
14
// Declare chdemko\BitArray namespace
15
namespace chdemko\BitArray;
16
17
/**
18
 * Array of bits
19
 *
20
 * @package BitArray
21
 *
22
 * @property-read integer  $count  The number of bits set to true
23
 * @property-read integer  $size   The number of bits
24
 *
25
 * @since 1.0.0
26
 */
27
class BitArray implements \ArrayAccess, \Countable, \IteratorAggregate, \JsonSerializable
28
{
29
    /**
30
     * @var integer[]  Number of bits for each value between 0 and 255
31
     */
32
    private static $count = array(
33
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
34
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
35
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
36
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
37
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
38
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
39
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
40
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
41
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
42
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
43
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
44
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
45
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
46
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
47
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
48
        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8
49
    );
50
51
    /**
52
     * @var integer[]  Mask for restricting complements
53
     */
54
    private static $restrict = array(255, 1, 3, 7, 15, 31, 63, 127);
55
56
    /**
57
     * @var string  Underlying data
58
     *
59
     * @since 1.0.0
60
     */
61
    private $data;
62
63
    /**
64
     * @var integer  Size of the bit array
65
     *
66
     * @since 1.0.0
67
     */
68
    private $size;
69
70
    /**
71
     * Create a new bit array of the given size
72
     *
73
     * @param integer $size    The BitArray size
74
     * @param boolean $default The default value for bits
75
     *
76
     * @since 1.0.0
77
     */
78
    protected function __construct($size = 0, $default = false)
79
    {
80
        $this->size = (int) $size;
0 ignored issues
show
Bug introduced by
The property size is declared read-only in chdemko\BitArray\BitArray.
Loading history...
81
82
        if ($default) {
83
            $this->data = str_repeat(chr(255), (int) ceil($this->size / 8));
84
            $this->restrict();
85
        } else {
86
            $this->data = str_repeat(chr(0), (int) ceil($this->size / 8));
87
        }
88
    }
89
90
    /**
91
     * Remove useless bits for simplifying count operation.
92
     *
93
     * @return BitArray  $this for chaining
94
     *
95
     * @since 1.2.0
96
     */
97
    protected function restrict()
98
    {
99
        $length = strlen($this->data);
100
101
        if ($length > 0) {
102
            $this->data[$length - 1] = chr(ord($this->data[$length - 1]) & self::$restrict[$this->size % 8]);
103
        }
104
105
        return $this;
106
    }
107
108
    /**
109
     * Clone a BitArray
110
     *
111
     * @return void
112
     *
113
     * @since 1.0.0
114
     */
115
    public function __clone()
116
    {
117
        $this->data = str_repeat($this->data, 1);
118
    }
119
120
    /**
121
     * Convert the object to a string
122
     *
123
     * @return string  String representation of this object
124
     *
125
     * @since 1.0.0
126
     */
127
    public function __toString()
128
    {
129
        $string = str_repeat('0', $this->size);
130
131
        for ($offset = 0; $offset < $this->size; $offset++) {
132
            if (ord($this->data[(int) ($offset / 8)]) & (1 << $offset % 8)) {
133
                $string[$offset] = '1';
134
            }
135
        }
136
137
        return $string;
138
    }
139
140
    /**
141
     * Magic get method
142
     *
143
     * @param string $property The property
144
     *
145
     * @throws RuntimeException  If the property does not exist
146
     *
147
     * @return mixed  The value associated to the property
148
     *
149
     * @since 1.0.0
150
     */
151
    public function __get($property)
152
    {
153
        switch ($property) {
154
            case 'size':
155
                return $this->size;
156
            case 'count':
157
                return $this->count();
158
            default:
159
                throw new \RuntimeException('Undefined property');
160
        }
161
    }
162
163
    /**
164
     * Test the existence of an index
165
     *
166
     * @param integer $offset The offset
167
     *
168
     * @return boolean  The truth value
169
     *
170
     * @since 1.0.0
171
     */
172
    public function offsetExists($offset): bool
173
    {
174
        return is_int($offset) && $offset >= 0 && $offset < $this->size;
175
    }
176
177
    /**
178
     * Get the truth value for an index
179
     *
180
     * @param integer $offset The offset
181
     *
182
     * @return boolean  The truth value
183
     *
184
     * @throws OutOfRangeException  Argument index must be an positive integer lesser than the size
185
     *
186
     * @since 1.0.0
187
     */
188
    public function offsetGet($offset): bool
189
    {
190
        if ($this->offsetExists($offset)) {
191
            return (bool) (ord($this->data[(int) ($offset / 8)]) & (1 << $offset % 8));
192
        } else {
193
            throw new \OutOfRangeException('Argument offset must be a positive integer lesser than the size');
194
        }
195
    }
196
197
    /**
198
     * Set the truth value for an index
199
     *
200
     * @param integer $offset The offset
201
     * @param boolean $value  The truth value
202
     *
203
     * @return void
204
     *
205
     * @throws OutOfRangeException  Argument index must be an positive integer lesser than the size
206
     *
207
     * @since 1.0.0
208
     */
209
    public function offsetSet($offset, $value): void
210
    {
211
        if ($this->offsetExists($offset)) {
212
            $index = (int) ($offset / 8);
213
214
            if ($value) {
215
                $this->data[$index] = chr(ord($this->data[$index]) | (1 << $offset % 8));
216
            } else {
217
                $this->data[$index] = chr(ord($this->data[$index]) & ~(1 << $offset % 8));
218
            }
219
        } else {
220
            throw new \OutOfRangeException('Argument index must be a positive integer lesser than the size');
221
        }
222
    }
223
224
    /**
225
     * Unset the existence of an index
226
     *
227
     * @param integer $offset The index
228
     *
229
     * @return void
230
     *
231
     * @throws RuntimeException  Values cannot be unset
232
     *
233
     * @since 1.0.0
234
     */
235
    public function offsetUnset($offset): void
236
    {
237
        throw new \RuntimeException('Values cannot be unset');
238
    }
239
240
    /**
241
     * Return the number of true bits
242
     *
243
     * @return integer  The number of true bits
244
     *
245
     * @since 1.0.0
246
     */
247
    public function count(): int
248
    {
249
        $count = 0;
250
251
        for ($index = 0, $length = strlen($this->data); $index < $length; $index++) {
252
            $count += self::$count[ord($this->data[$index])];
253
        }
254
255
        return $count;
256
    }
257
258
    /**
259
     * Transform the object to an array
260
     *
261
     * @return array  Array of values
262
     *
263
     * @since 1.1.0
264
     */
265
    public function toArray()
266
    {
267
        $array = array();
268
269
        for ($index = 0; $index < $this->size; $index++) {
270
            $array[] = (bool) (ord($this->data[(int) ($index / 8)]) & (1 << $index % 8));
271
        }
272
273
        return $array;
274
    }
275
276
    /**
277
     * Serialize the object
278
     *
279
     * @return array  Array of values
280
     *
281
     * @since 1.0.0
282
     */
283
    public function jsonSerialize(): array
284
    {
285
        return $this->toArray();
286
    }
287
288
    /**
289
     * Get an iterator
290
     *
291
     * @return Iterator  Iterator
292
     *
293
     * @since 1.0.0
294
     */
295
    public function getIterator(): Iterator
296
    {
297
        return new Iterator($this);
298
    }
299
300
    /**
301
     * Return the size
302
     *
303
     * @return integer  The size
304
     *
305
     * @since 1.0.0
306
     */
307
    public function size()
308
    {
309
        return $this->size;
310
    }
311
312
    /**
313
     * Copy bits directly from a BitArray
314
     *
315
     * @param BitArray $bits   A BitArray to copy
316
     * @param int      $index  Starting index for destination
317
     * @param int      $offset Starting index for copying
318
     * @param int      $size   Copy size
319
     *
320
     * @return BitArray  This object for chaining
321
     *
322
     * @throws OutOfRangeException  Argument index must be an positive integer lesser than the size
323
     *
324
     * @since 1.1.0
325
     */
326
    public function directCopy(BitArray $bits, $index = 0, $offset = 0, $size = 0)
327
    {
328
        if ($offset > $index) {
329
            for ($i = 0; $i < $size; $i++) {
330
                $this[$i + $index] = $bits[$i + $offset];
331
            }
332
        } else {
333
            for ($i = $size - 1; $i >= 0; $i--) {
334
                $this[$i + $index] = $bits[$i + $offset];
335
            }
336
        }
337
338
        return $this;
339
    }
340
341
    /**
342
     * Copy bits from a BitArray
343
     *
344
     * * if index is non-negative, the index parameter is used as it is,
345
     *   keeping its real value between 0 and size-1;
346
     * * if index is negative, the index parameter starts from the end,
347
     *   keeping its real value between 0 and size-1.
348
     *
349
     * * if offset is non-negative, the offset parameter is used as it is,
350
     *   keeping its positive value between 0 and size-1;
351
     * * if offset is negative, the offset parameter starts from the end,
352
     *   keeping its real value between 0 and size-1.
353
     *
354
     * * if size is given and is positive, then the copy will copy size elements.
355
     * * if the bits argument is shorter than the size, then only the available elements will be copied.
356
     * * if size is given and is negative, then the copy starts from the end.
357
     * * if size is omitted, then the copy will have everything from offset up
358
     *   until the end of the bits argument.
359
     *
360
     * @param BitArray $bits   A BitArray to copy
361
     * @param int      $index  Starting index for destination.
362
     * @param int      $offset Starting index for copying.
363
     * @param mixed    $size   Copy size.
364
     *
365
     * @return BitArray  This object for chaining
366
     *
367
     * @since 1.1.0
368
     */
369
    public function copy(BitArray $bits, $index = 0, $offset = 0, $size = null)
370
    {
371
        $index = $this->getRealOffset($index);
372
        $offset = $bits->getRealOffset($offset);
373
        $size = $bits->getRealSize($offset, $size);
374
375
        if ($size > $this->size - $index) {
376
            $size = $this->size - $index;
377
        }
378
379
        return $this->directCopy($bits, $index, $offset, $size);
380
    }
381
382
    /**
383
     * Get the real offset using a positive or negative offset
384
     *
385
     * * If offset is non-negative, the offset parameter is used as it is,
386
     *   keeping its real value between 0 and size-1.
387
     * * if offset is negative, the offset parameter starts from the end,
388
     *   keeping its real value between 0 and size-1.
389
     *
390
     * @param int $offset The offset
391
     *
392
     * @return integer  The real offset
393
     *
394
     * @since 1.1.0
395
     */
396
    protected function getRealOffset($offset)
397
    {
398
        $offset = (int) $offset;
399
400
        if ($offset < 0) {
401
            // Start from the end
402
            $offset = $this->size + $offset;
403
404
            if ($offset < 0) {
405
                $offset = 0;
406
            }
407
        } elseif ($offset > $this->size) {
408
            $offset = $this->size;
409
        }
410
411
        return $offset;
412
    }
413
414
    /**
415
     * Get the real offset using a positive or negative offset
416
     *
417
     * * if size is given and is positive, then the real size will be between 0 and the current size-1.
418
     * * if size is given and is negative, then the real size starts from the end.
419
     * * if size is omitted, then the size goes until the end of the BitArray.
420
     *
421
     * @param int   $offset The real offset.
422
     * @param mixed $size   The size
423
     *
424
     * @return integer  The real size
425
     *
426
     * @since 1.1.0
427
     */
428
    protected function getRealSize($offset, $size)
429
    {
430
        if ($size === null) {
431
            $size = $this->size - $offset;
432
        } else {
433
            $size = (int) $size;
434
435
            if ($size < 0) {
436
                $size = $this->size + $size - $offset;
437
438
                if ($size < 0) {
439
                    $size = 0;
440
                }
441
            } elseif ($size > $this->size - $offset) {
442
                $size = $this->size - $offset;
443
            }
444
        }
445
446
        return $size;
447
    }
448
449
    /**
450
     * Create a new BitArray from an integer
451
     *
452
     * @param integer $size    Size of the BitArray
453
     * @param boolean $default The default value for bits
454
     *
455
     * @return BitArray  A new BitArray
456
     *
457
     * @since 1.0.0
458
     */
459
    public static function fromInteger($size, $default = false)
460
    {
461
        return new BitArray($size, (bool) $default);
462
    }
463
464
    /**
465
     * Create a new BitArray from a sequence of bits.
466
     *
467
     * @param integer $size   Size of the BitArray
468
     * @param integer $values The values for the bits
469
     *
470
     * @return BitArray  A new BitArray
471
     *
472
     * @since 1.2.0
473
     */
474
    public static function fromDecimal($size, $values = 0)
475
    {
476
        $php_bit_size = PHP_INT_SIZE * 8;
477
        $size = min((int) $size, $php_bit_size);
478
        $values <<= ($php_bit_size - $size);
479
        $bits = new BitArray($size);
480
481
        for ($i = 0; $i < ceil($size / 8); $i++) {
482
            $value = ($values & (0xff << ($php_bit_size - 8))) >> ($php_bit_size - 8);
483
            $reverse = 0;
484
            for ($j = 0; $j < 8; $j++) {
485
                if ($value & (1 << $j)) {
486
                    $reverse |= 1 << (7 - $j);
487
                }
488
            }
489
            $bits->data[$i] = chr($reverse);
490
            $values <<= 8;
491
        }
492
493
        return $bits;
494
    }
495
496
    /**
497
     * Create a new BitArray from a traversable
498
     *
499
     * @param mixed $traversable A traversable and countable
500
     *
501
     * @return BitArray  A new BitArray
502
     *
503
     * @since 1.0.0
504
     */
505
    public static function fromTraversable($traversable)
506
    {
507
        $bits = new BitArray(count($traversable));
508
        $offset = 0;
509
        $ord = 0;
510
511
        foreach ($traversable as $value) {
512
            if ($value) {
513
                $ord |= 1 << $offset % 8;
514
            }
515
516
            if ($offset % 8 === 7) {
517
                $bits->data[(int) ($offset / 8)] = chr($ord);
518
                $ord = 0;
519
            }
520
521
            $offset++;
522
        }
523
524
        if ($offset % 8 !== 0) {
525
            $bits->data[(int) ($offset / 8)] = chr($ord);
526
        }
527
528
        return $bits;
529
    }
530
531
    /**
532
     * Create a new BitArray from a bit string
533
     *
534
     * @param string $string A bit string
535
     *
536
     * @return BitArray  A new BitArray
537
     *
538
     * @since 1.0.0
539
     */
540
    public static function fromString($string)
541
    {
542
        $bits = new BitArray(strlen($string));
543
        $ord = 0;
544
545
        for ($offset = 0; $offset < $bits->size; $offset++) {
546
            if ($string[$offset] !== '0') {
547
                $ord |= 1 << $offset % 8;
548
            }
549
550
            if ($offset % 8 === 7) {
551
                $bits->data[(int) ($offset / 8)] = chr($ord);
552
                $ord = 0;
553
            }
554
        }
555
556
        if ($offset % 8 !== 0) {
557
            $bits->data[(int) ($offset / 8)] = chr($ord);
558
        }
559
560
        return $bits;
561
    }
562
563
    /**
564
     * Create a new BitArray from json
565
     *
566
     * @param string $json A json encoded value
567
     *
568
     * @return BitArray  A new BitArray
569
     *
570
     * @since 1.0.0
571
     */
572
    public static function fromJson($json)
573
    {
574
        return self::fromTraversable(json_decode($json));
575
    }
576
577
    /**
578
     * Create a new BitArray using a slice
579
     *
580
     * * if offset is non-negative, the slice will start at that offset in the bits argument.
581
     * * if offset is negative, the slice will start from the end of the bits argument.
582
     *
583
     * * if size is given and is positive, then the slice will have up to that many elements in it.
584
     * * if the bits argument is shorter than the size, then only the available elements will be present.
585
     * * if size is given and is negative,
586
     *   then the slice will stop that many elements from the end of the bits argument.
587
     * * if size is omitted,
588
     *   then the slice will have everything from offset up until the end of the bits argument.
589
     *
590
     * @param BitArray $bits   A BitArray to get the slice
591
     * @param int      $offset The offset
592
     * @param mixed    $size   The size
593
     *
594
     * @return BitArray  A new BitArray
595
     *
596
     * @since 1.1.0
597
     */
598
    public static function fromSlice(BitArray $bits, $offset = 0, $size = null)
599
    {
600
        $offset = $bits->getRealOffset($offset);
601
        $size = $bits->getRealSize($offset, $size);
602
        $slice = new BitArray($size);
603
604
        return $slice->directCopy($bits, 0, $offset, $size);
605
    }
606
607
    /**
608
     * Create a new BitArray using the concat operation
609
     *
610
     * @param BitArray $bits1 A BitArray
611
     * @param BitArray $bits2 A BitArray
612
     *
613
     * @return BitArray  A new BitArray
614
     *
615
     * @since 1.1.0
616
     */
617
    public static function fromConcat(BitArray $bits1, BitArray $bits2)
618
    {
619
        $size = $bits1->size + $bits2->size;
620
        $concat = new BitArray($size);
621
        $concat->directCopy($bits1, 0, 0, $bits1->size);
622
        $concat->directCopy($bits2, $bits1->size, 0, $bits2->size);
623
624
        return $concat;
625
    }
626
627
    /**
628
     * Complement the bit array
629
     *
630
     * @return BitArray  This object for chaining
631
     *
632
     * @since 1.0.0
633
     */
634
    public function applyComplement()
635
    {
636
        $length = strlen($this->data);
637
638
        for ($index = 0; $index < $length; $index++) {
639
            $this->data[$index] = chr(~ ord($this->data[$index]));
640
        }
641
642
        return $this->restrict();
643
    }
644
645
    /**
646
     * Or with an another bit array
647
     *
648
     * @param BitArray $bits A bit array
649
     *
650
     * @return BitArray  This object for chaining
651
     *
652
     * @throws InvalidArgumentException  Argument must be of equal size
653
     *
654
     * @since 1.0.0
655
     */
656
    public function applyOr(BitArray $bits)
657
    {
658
        if ($this->size == $bits->size) {
659
            $length = strlen($this->data);
660
661
            for ($index = 0; $index < $length; $index++) {
662
                $this->data[$index] = chr(ord($this->data[$index]) | ord($bits->data[$index]));
663
            }
664
665
            return $this;
666
        } else {
667
            throw new \InvalidArgumentException('Argument must be of equal size');
668
        }
669
    }
670
671
    /**
672
     * And with an another bit array
673
     *
674
     * @param BitArray $bits A bit array
675
     *
676
     * @return BitArray  This object for chaining
677
     *
678
     * @throws InvalidArgumentException  Argument must be of equal size
679
     *
680
     * @since 1.0.0
681
     */
682
    public function applyAnd(BitArray $bits)
683
    {
684
        if ($this->size == $bits->size) {
685
            $length = strlen($this->data);
686
687
            for ($index = 0; $index < $length; $index++) {
688
                $this->data[$index] = chr(ord($this->data[$index]) & ord($bits->data[$index]));
689
            }
690
691
            return $this;
692
        } else {
693
            throw new \InvalidArgumentException('Argument must be of equal size');
694
        }
695
    }
696
697
    /**
698
     * Xor with an another bit array
699
     *
700
     * @param BitArray $bits A bit array
701
     *
702
     * @return BitArray  This object for chaining
703
     *
704
     * @throws InvalidArgumentException  Argument must be of equal size
705
     *
706
     * @since 1.0.0
707
     */
708
    public function applyXor(BitArray $bits)
709
    {
710
        if ($this->size == $bits->size) {
711
            $length = strlen($this->data);
712
713
            for ($index = 0; $index < $length; $index++) {
714
                $this->data[$index] = chr(ord($this->data[$index]) ^ ord($bits->data[$index]));
715
            }
716
717
            return $this;
718
        } else {
719
            throw new \InvalidArgumentException('Argument must be of equal size');
720
        }
721
    }
722
723
    /**
724
     * Shift a bit array.
725
     *
726
     * Negative value means the shifting is done right to left
727
     * while positive value means the shifting is done left to right.
728
     *
729
     * @param int     $size  Size to shift.
730
     *
731
     * @param boolean $value Value to shift
732
     *
733
     * @return BitArray  $this for chaining
734
     *
735
     * @since 1.2.0
736
     */
737
    public function shift($size = 1, $value = false)
738
    {
739
        $size = (int) $size;
740
741
        if ($size > 0) {
742
            $min = min($this->size, $size);
743
744
            for ($i = $this->size - 1; $i >= $min; $i--) {
745
                $this[$i] = $this[$i - $min];
746
            }
747
748
            for ($i = 0; $i < $min; $i++) {
749
                $this[$i] = $value;
750
            }
751
        } else {
752
            $min = min($this->size, -$size);
753
754
            for ($i = 0; $i < $this->size - $min; $i++) {
755
                $this[$i] = $this[$i + $min];
756
            }
757
758
            for ($i = $this->size - $min; $i < $this->size; $i++) {
759
                $this[$i] = $value;
760
            }
761
        }
762
763
        return $this;
764
    }
765
}
766