Add BoolList class. (#120)
Adds `BoolList` class which implements a list of booleans with one bit of storage per value.
diff --git a/lib/collection.dart b/lib/collection.dart
index 27a75c8..f6f4ae3 100644
--- a/lib/collection.dart
+++ b/lib/collection.dart
@@ -4,6 +4,7 @@
export 'src/algorithms.dart'
show binarySearch, insertionSort, lowerBound, mergeSort, shuffle, reverse;
+export 'src/boollist.dart';
export 'src/canonicalized_map.dart';
export 'src/combined_wrappers/combined_iterable.dart';
export 'src/combined_wrappers/combined_list.dart';
diff --git a/lib/src/boollist.dart b/lib/src/boollist.dart
new file mode 100644
index 0000000..cf23b57
--- /dev/null
+++ b/lib/src/boollist.dart
@@ -0,0 +1,258 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:collection' show ListMixin;
+import 'dart:typed_data' show Uint32List;
+
+import 'unmodifiable_wrappers.dart' show NonGrowableListMixin;
+
+/// A space-efficient list of boolean values.
+///
+/// Uses list of integers as internal storage to reduce memory usage.
+abstract class BoolList with ListMixin<bool> {
+ static const int _entryShift = 5;
+
+ static const int _bitsPerEntry = 32;
+
+ static const int _entrySignBitIndex = 31;
+
+ int _length;
+
+ Uint32List _data;
+
+ BoolList._(this._data, this._length);
+
+ factory BoolList._selectType(int length, bool growable) {
+ if (growable) {
+ return _GrowableBoolList(length);
+ } else {
+ return _NonGrowableBoolList(length);
+ }
+ }
+
+ /// Creates a list of booleans with the provided length.
+ ///
+ /// The list is initially filled with the [fill] value, and
+ /// the list is growable if [growable] is true.
+ factory BoolList(int length, {bool fill = false, bool growable = false}) {
+ RangeError.checkNotNegative(length, 'length');
+
+ BoolList boolist;
+ if (growable) {
+ boolist = _GrowableBoolList(length);
+ } else {
+ boolist = _NonGrowableBoolList(length);
+ }
+
+ if (fill) {
+ boolist.fillRange(0, length, true);
+ }
+
+ return boolist;
+ }
+
+ /// Creates an empty list of booleans.
+ ///
+ /// The list defaults to being growable unless [growable] is `false`.
+ /// If [capacity] is provided, and [growable] is not `false`,
+ /// the implementation will attempt to make space for that
+ /// many elements before needing to grow its internal storage.
+ factory BoolList.empty({bool growable = true, int capacity = 0}) {
+ RangeError.checkNotNegative(capacity, 'length');
+
+ if (growable) {
+ return _GrowableBoolList._withCapacity(0, capacity);
+ } else {
+ return _NonGrowableBoolList._withCapacity(0, capacity);
+ }
+ }
+
+ /// Generates a [BoolList] of values.
+ ///
+ /// Creates a [BoolList] with [length] positions and fills it with values created by
+ /// calling [generator] for each index in the range `0` .. `length - 1` in increasing order.
+ ///
+ /// The created list is fixed-length unless [growable] is true.
+ factory BoolList.generate(
+ int length,
+ bool Function(int) generator, {
+ bool growable = true,
+ }) {
+ RangeError.checkNotNegative(length, 'length');
+
+ var instance = BoolList._selectType(length, growable);
+ for (var i = 0; i < length; i++) {
+ instance._setBit(i, generator(i));
+ }
+ return instance;
+ }
+
+ /// Creates a list containing all [elements].
+ ///
+ /// The [Iterator] of [elements] provides the order of the elements.
+ ///
+ /// This constructor creates a growable [BoolList] when [growable] is true;
+ /// otherwise, it returns a fixed-length list.
+ factory BoolList.from(Iterable<bool> elements, {bool growable = false}) {
+ return BoolList._selectType(elements.length, growable)..setAll(0, elements);
+ }
+
+ @override
+ int get length => _length;
+
+ @override
+ bool operator [](int index) {
+ RangeError.checkValidIndex(index, this, 'index', _length);
+ return (_data[index >> _entryShift] &
+ (1 << (index & _entrySignBitIndex))) !=
+ 0;
+ }
+
+ @override
+ void operator []=(int index, bool val) {
+ RangeError.checkValidIndex(index, this, 'index', _length);
+ _setBit(index, val);
+ }
+
+ @override
+ void fillRange(int start, int end, [bool? fill]) {
+ RangeError.checkValidRange(start, end, _length);
+ fill ??= false;
+
+ var startWord = start >> _entryShift;
+ var endWord = (end - 1) >> _entryShift;
+
+ var startBit = start & _entrySignBitIndex;
+ var endBit = (end - 1) & _entrySignBitIndex;
+
+ if (startWord < endWord) {
+ if (fill) {
+ _data[startWord] |= -1 << startBit;
+ _data.fillRange(startWord + 1, endWord, -1);
+ _data[endWord] |= (1 << (endBit + 1)) - 1;
+ } else {
+ _data[startWord] &= (1 << startBit) - 1;
+ _data.fillRange(startWord + 1, endWord, 0);
+ _data[endWord] &= -1 << (endBit + 1);
+ }
+ } else {
+ if (fill) {
+ _data[startWord] |= ((1 << (endBit - startBit + 1)) - 1) << startBit;
+ } else {
+ _data[startWord] &= ((1 << startBit) - 1) | (-1 << (endBit + 1));
+ }
+ }
+ }
+
+ /// Returns custom iterator for [BoolList].
+ ///
+ /// To provide null safety [Iterator.current] getter of returned iterator
+ /// returns `false` before and after iteration process.
+ @override
+ Iterator<bool> get iterator => _BoolListIterator(this);
+
+ void _setBit(int index, bool val) {
+ if (val) {
+ _data[index >> _entryShift] |= 1 << (index & _entrySignBitIndex);
+ } else {
+ _data[index >> _entryShift] &= ~(1 << (index & _entrySignBitIndex));
+ }
+ }
+
+ static int _lengthInWords(int bitsLength) {
+ return (bitsLength + (_bitsPerEntry - 1)) >> _entryShift;
+ }
+}
+
+class _GrowableBoolList extends BoolList {
+ static const int _growthFactor = 2;
+
+ _GrowableBoolList._withCapacity(int length, int capacity)
+ : super._(
+ Uint32List(BoolList._lengthInWords(capacity)),
+ length,
+ );
+
+ _GrowableBoolList(int length)
+ : super._(
+ Uint32List(BoolList._lengthInWords(length * _growthFactor)),
+ length,
+ );
+
+ @override
+ set length(int length) {
+ RangeError.checkNotNegative(length, 'length');
+ if (length > _length) {
+ _expand(length);
+ } else if (length < _length) {
+ _shrink(length);
+ }
+ }
+
+ void _expand(int length) {
+ if (length > _data.length * BoolList._bitsPerEntry) {
+ _data = Uint32List(
+ BoolList._lengthInWords(length * _growthFactor),
+ )..setAll(0, _data);
+ }
+ _length = length;
+ }
+
+ void _shrink(int length) {
+ if (length < _length ~/ _growthFactor) {
+ var newDataLength = BoolList._lengthInWords(length);
+ _data = Uint32List(newDataLength)..setRange(0, newDataLength, _data);
+ }
+
+ for (var i = length; i < _data.length * BoolList._bitsPerEntry; i++) {
+ _setBit(i, false);
+ }
+
+ _length = length;
+ }
+}
+
+class _NonGrowableBoolList extends BoolList with NonGrowableListMixin<bool> {
+ _NonGrowableBoolList._withCapacity(int length, int capacity)
+ : super._(
+ Uint32List(BoolList._lengthInWords(capacity)),
+ length,
+ );
+
+ _NonGrowableBoolList(int length)
+ : super._(
+ Uint32List(BoolList._lengthInWords(length)),
+ length,
+ );
+}
+
+class _BoolListIterator implements Iterator<bool> {
+ bool _current = false;
+ int _pos = 0;
+ final int _length;
+
+ final BoolList _boolList;
+
+ _BoolListIterator(this._boolList) : _length = _boolList._length;
+
+ @override
+ bool get current => _current;
+
+ @override
+ bool moveNext() {
+ if (_boolList._length != _length) {
+ throw ConcurrentModificationError(_boolList);
+ }
+
+ if (_pos < _boolList.length) {
+ var pos = _pos++;
+ _current = _boolList._data[pos >> BoolList._entryShift] &
+ (1 << (pos & BoolList._entrySignBitIndex)) !=
+ 0;
+ return true;
+ }
+ _current = false;
+ return false;
+ }
+}
diff --git a/test/boollist_test.dart b/test/boollist_test.dart
new file mode 100644
index 0000000..263186c
--- /dev/null
+++ b/test/boollist_test.dart
@@ -0,0 +1,162 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Tests for BoolList.
+
+import 'package:collection/collection.dart';
+import 'package:test/test.dart';
+
+void main() {
+ bool generator(int index) {
+ if (index < 512) {
+ return index.isEven;
+ }
+ return false;
+ }
+
+ test('BoolList()', () {
+ expect(BoolList(1024, fill: false), List.filled(1024, false));
+
+ expect(BoolList(1024, fill: true), List.filled(1024, true));
+ });
+
+ test('BoolList.empty()', () {
+ expect(BoolList.empty(growable: true, capacity: 1024), []);
+
+ expect(BoolList.empty(growable: false, capacity: 1024), []);
+ });
+
+ test('BoolList.generate()', () {
+ expect(
+ BoolList.generate(1024, generator),
+ List.generate(1024, generator),
+ );
+ });
+
+ test('BoolList.from()', () {
+ var src = List.generate(1024, generator);
+ expect(BoolList.from(src), src);
+ });
+
+ group('[], []=', () {
+ test('RangeError', () {
+ var b = BoolList(1024, fill: false);
+
+ expect(() {
+ b[-1];
+ }, throwsRangeError);
+
+ expect(() {
+ b[1024];
+ }, throwsRangeError);
+ });
+
+ test('[], []=', () {
+ var b = BoolList(1024, fill: false);
+
+ bool posVal;
+ for (var pos = 0; pos < 1024; ++pos) {
+ posVal = generator(pos);
+ b[pos] = posVal;
+ expect(b[pos], posVal, reason: 'at pos $pos');
+ }
+ });
+ });
+
+ group('length', () {
+ test('shrink length', () {
+ var b = BoolList(1024, fill: true, growable: true);
+
+ b.length = 768;
+ expect(b, List.filled(768, true));
+
+ b.length = 128;
+ expect(b, List.filled(128, true));
+
+ b.length = 0;
+ expect(b, []);
+ });
+
+ test('expand from != 0', () {
+ var b = BoolList(256, fill: true, growable: true);
+
+ b.length = 384;
+ expect(b, List.filled(384, false)..fillRange(0, 256, true));
+
+ b.length = 2048;
+ expect(b, List.filled(2048, false)..fillRange(0, 256, true));
+ });
+
+ test('expand from = 0', () {
+ var b = BoolList(0, growable: true);
+ expect(b.length, 0);
+
+ b.length = 256;
+ expect(b, List.filled(256, false));
+ });
+
+ test('throw UnsupportedError', () {
+ expect(() {
+ BoolList(1024).length = 512;
+ }, throwsUnsupportedError);
+ });
+ });
+
+ group('fillRange', () {
+ test('In one word', () {
+ expect(
+ BoolList(1024)..fillRange(32, 64, true),
+ List.filled(1024, false)..fillRange(32, 64, true),
+ );
+
+ expect(
+ // BoolList.filled constructor isn't used due internal usage of fillRange
+ BoolList.generate(1024, (i) => true)..fillRange(32, 64, false),
+ List.filled(1024, true)..fillRange(32, 64, false),
+ );
+ });
+
+ test('In several words', () {
+ expect(
+ BoolList(1024)..fillRange(32, 128, true),
+ List.filled(1024, false)..fillRange(32, 128, true),
+ );
+
+ expect(
+ // BoolList.filled constructor isn't used due internal usage of fillRange
+ BoolList.generate(1024, (i) => true)..fillRange(32, 128, false),
+ List.filled(1024, true)..fillRange(32, 128, false),
+ );
+ });
+ });
+
+ group('Iterator', () {
+ test('Iterator', () {
+ var b = BoolList.generate(1024, generator);
+ var iter = b.iterator;
+
+ expect(iter.current, false);
+ for (var i = 0; i < 1024; i++) {
+ expect(iter.moveNext(), true);
+
+ expect(iter.current, generator(i), reason: 'at pos $i');
+ }
+
+ expect(iter.moveNext(), false);
+ expect(iter.current, false);
+ });
+
+ test('throw ConcurrentModificationError', () {
+ var b = BoolList(1024, fill: true, growable: true);
+
+ var iter = b.iterator;
+
+ iter.moveNext();
+ b.length = 512;
+ expect(() {
+ iter.moveNext();
+ }, throwsConcurrentModificationError);
+ });
+ });
+}