diff --git a/CHANGELOG.md b/CHANGELOG.md index 98895fd..0c99059 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,5 @@ # CHANGELOG -## 4.0.0-alpha - > [!IMPORTANT] > Version 4.0.0 has a _large_ set of breaking changes, including removing the > vast majority of extension methods and boxed classes, in favor of using the @@ -11,11 +9,35 @@ [file an issue]: https://github.com/matanlurey/binary.dart/issues +## 4.0.0-alpha+1 + +**New features**: + +- Added `BitList`, a compact `List` implementation that stores every + element as a single bit, with implementations that are fixed-size and + growable. + +- Added `.zero` and `.one` as static constants. + +- Added `collectBytes()`, a utility to convert a `Stream>` into a + `Uint8List`. + +**Breaking changes**: + +- Replaced `.bits` with `.toBitList()`: + + ```diff + - final bits = Int8(0).bits; + + final bits = Int8(0).toBitList(); + ``` + +## 4.0.0-alpha + **New features**: Lots and lots. It will be easier to just read the API documentation. -**Breaking changes:** +**Breaking changes**: _Basically everything_. The entire API has been restructured to use extension types, and some APIs removed entirely that were either not well-thought out diff --git a/README.md b/README.md index 4cb70cc..e6eb963 100644 --- a/README.md +++ b/README.md @@ -156,8 +156,8 @@ integers. [pub_img]: https://img.shields.io/pub/v/binary.svg [gha_url]: https://github.com/matanlurey/binary.dart/actions [gha_img]: https://github.com/matanlurey/binary.dart/actions/workflows/check.yaml/badge.svg -[cov_url]: https://codecov.io/gh/matanlurey/binary.dart -[cov_img]: https://codecov.io/gh/matanlurey/binary.dart/branch/main/graph/badge.svg +[cov_url]: https://coveralls.io/github/matanlurey/binary.dart?branch=main +[cov_img]: https://coveralls.io/repos/github/matanlurey/binary.dart/badge.svg?branch=main [doc_url]: https://www.dartdocs.org/documentation/binary/latest [doc_img]: https://img.shields.io/badge/Documentation-binary-blue.svg [sty_url]: https://pub.dev/packages/oath diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..1f53482 --- /dev/null +++ b/example/README.md @@ -0,0 +1,39 @@ +# Examples + +This directory contains examples of how to use `package:binary`. + +## [Basic](./basic.dart) + +A simple example of how to use `package:binary` to parse and shift bits. + +```shell +$ dart example/basic.dart + +127 +00000011 +``` + +## [Checksum](./checksum.dart) + +Calculates the CRC32 checksum of a file. + +```shell +$ dart example/checksum.dart example/checksum.dart + +CRC32 checksum: 13b53c2f +``` + +## [Bitfield](./bitfield.dart) + +Demonstrates decoding binary data efficiently, i.e. opcodes or binary formats. + +```shell +$ dart example/bitfield.dart + +11011101 + +Kind: 00001101 +S: 00000001 +Level: 00000010 +P: 00000001 +``` diff --git a/example/example.dart b/example/basic.dart similarity index 100% rename from example/example.dart rename to example/basic.dart diff --git a/example/bitfield.dart b/example/bitfield.dart new file mode 100644 index 0000000..006259a --- /dev/null +++ b/example/bitfield.dart @@ -0,0 +1,36 @@ +import 'package:binary/binary.dart' show Uint8; + +/// Let's assume we have the following format: +/// ```txt +/// 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 +/// - | ----- | - | ------------- +/// P | Level | S | Kind +/// ``` +void main() { + // Create a bit field with the format. + final field = Uint8.zero + .replace(0, 3, int.parse('1101', radix: 2)) // Kind + .replace(4, 4, int.parse('1', radix: 2)) // S + .replace(5, 6, int.parse('10', radix: 2)) // Level + .replace(7, 7, int.parse('1', radix: 2)); // P + + // P S + // vv v + print(field.toBinaryString()); // 11011101 + // ^^ ^^^^ + // Level Kind + + // Now, read it back. + final (kind, s, level, p) = ( + field.slice(0, 3), + field.slice(4, 4), + field.slice(5, 6), + field.slice(7, 7), + ); + + print(''); + print('Kind: ${kind.toBinaryString()}'); + print('S: ${s.toBinaryString()}'); + print('Level: ${level.toBinaryString()}'); + print('P: ${p.toBinaryString()}'); +} diff --git a/example/checksum.dart b/example/checksum.dart new file mode 100644 index 0000000..805d65b --- /dev/null +++ b/example/checksum.dart @@ -0,0 +1,42 @@ +import 'dart:convert' show utf8; +import 'dart:typed_data'; + +import 'package:binary/binary.dart'; + +/// Examples of commom checksum algorithms using `package:binary`. +void main() { + // Calculate the CRC32 checksum of a list of bytes. + final data = Uint8List.fromList(utf8.encode('Hello, World!')); + final checksum = crc32(data); + print('CRC32 checksum: ${checksum.toRadixString(16)}'); +} + +/// Calculate the CRC32 checksum of a list of bytes using `CRC-32/JAMCRC`. +int crc32(Uint8List data, [Uint32 initialCrc = Uint32.max]) { + var crc = initialCrc; + for (final byte in data) { + final index = (crc.toInt() ^ byte) & 0xFF; + crc = (crc >> 8) ^ _crc32Table[index]; + } + return crc.toInt(); +} + +final List _crc32Table = (() { + // We can freely cast a Uint32List to a List (zero-cost). + final table = Uint32List(256) as List; + + // Precompute the CRC32 table. + for (var i = 0; i < 256; i++) { + // Cost-free conversion from int to Uint32. + var crc = Uint32.fromUnchecked(i); + for (var j = 0; j < 8; j++) { + if (crc.nthBit(0)) { + crc = (crc >> 1) ^ const Uint32.fromUnchecked(0xEDB88320); + } else { + crc >>= 1; + } + } + table[i] = crc; + } + return table; +})(); diff --git a/lib/binary.dart b/lib/binary.dart index 2e9c44e..0b80be8 100644 --- a/lib/binary.dart +++ b/lib/binary.dart @@ -18,6 +18,8 @@ /// [little endian]: https://en.wikipedia.org/wiki/Endianness library; +export 'src/async.dart' show collectBytes; +export 'src/bit_list.dart'; export 'src/descriptor.dart'; export 'src/extension.dart'; export 'src/int16.dart'; diff --git a/lib/src/async.dart b/lib/src/async.dart new file mode 100644 index 0000000..989e7fb --- /dev/null +++ b/lib/src/async.dart @@ -0,0 +1,25 @@ +import 'dart:async'; +import 'dart:typed_data'; + +/// Collects the bytes of a `Stream>` into a [Uint8List]. +/// +/// This function returns a `Future` that completes when the stream +/// is done, and a `Future Function()` that can be used to cancel the +/// collection and underlying stream early. +(Future, Future Function() cancel) collectBytes( + Stream> stream, +) { + final completer = Completer.sync(); + final builder = BytesBuilder(copy: false); + late final StreamSubscription sub; + sub = stream.listen( + builder.add, + onDone: () async { + completer.complete(builder.toBytes()); + await sub.cancel(); + }, + onError: completer.completeError, + cancelOnError: true, + ); + return (completer.future, sub.cancel); +} diff --git a/lib/src/bit_list.dart b/lib/src/bit_list.dart new file mode 100644 index 0000000..9d54af5 --- /dev/null +++ b/lib/src/bit_list.dart @@ -0,0 +1,258 @@ +import 'dart:collection'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:binary/src/uint32.dart'; + +/// An variant of `List` that ensures each `bool` takes one bit of memory. +abstract interface class BitList implements List { + /// Creates a list of booleans with the provided [length]. + /// + /// The list is initially filled with `false` unless [fill] is set to `true`. + /// + /// If [growable] is `true`, the list will be able to grow beyond its initial + /// length. + @pragma('dart2js:tryInline') + @pragma('vm:prefer-inline') + factory BitList(int length, {bool fill = false, bool growable = false}) { + if (!growable) { + if (length <= 32) { + return _FixedUint32BitList._( + fill ? Uint32.max : Uint32.min, + length: length, + ); + } else { + return _FixedUint32ListBitList(length, fill: fill); + } + } + return _GrowableUint32ListBitList(length, fill: fill); + } + + /// Creates a list of booleans from the provided [bits]. + /// + /// If [growable] is `true`, the list will be able to grow beyond its initial + /// length. + factory BitList.from(Iterable bits, {bool growable = false}) { + var list = []; + var size = 0; + for (final bit in bits) { + if (size % 32 == 0) { + list.add(Uint32.min); + } + list[size ~/ 32] = list[size ~/ 32].setNthBit(size % 32, bit); + size++; + } + if (!growable) { + if (size <= 32) { + return _FixedUint32BitList._(list.first, length: size); + } else { + list = Uint32List.fromList(list as List) as List; + return _FixedUint32ListBitList._(list, length: size); + } + } else { + final bits = _GrowableUint32ListBitList(size, fill: false); + bits._bytes = Uint32List.fromList(list as List) as List; + bits._length = size; + return bits; + } + } + + /// Creates a list of booleans that uses the provided integer as initial bits. + /// + /// If [length] is not provided, it is assumed to be 32, and the top 32 bits + /// are used; otherwise, the top [length] bits are used, which cannot exceed + /// 32. + /// + /// If [growable] is `true`, the list will be able to grow beyond its initial + /// length. + factory BitList.fromInt(int bits, {int length = 32, bool growable = false}) { + RangeError.checkValueInInterval(length, 0, 32, 'length'); + // Mask off the top {{length}} bits. + bits &= (1 << length) - 1; + if (!growable) { + return _FixedUint32BitList._(Uint32.fromUnchecked(bits), length: length); + } + final list = _GrowableUint32ListBitList(length, fill: false); + list._bytes[0] = Uint32.fromUnchecked(bits); + return list; + } + + /// Returns a reference to the underlying bytes. + /// + /// By default, a copy of the bytes is returned. If [copy] is set to `false`, + /// the original bytes are returned; if so, if either the returned list, or + /// the bit list is modified, the other should be considered invalid. + Uint32List toUint32List({bool copy = true}); +} + +/// A fixed-length list of bits that is stored in a single 32-bit integer. +final class _FixedUint32BitList with ListBase implements BitList { + _FixedUint32BitList._( + this._bits, { + required this.length, + }) : assert(length > 0 && length <= 32, 'Length must be between 1 and 32.'); + + /// Backing integer that stores the bits, up to [length] bits. + Uint32 _bits; + + @override + final int length; + + @override + void add(bool value) { + throw UnsupportedError('Cannot add to a fixed-length list.'); + } + + @override + bool operator [](int index) => _bits.nthBit(index); + + @override + void operator []=(int index, bool value) { + _bits = _bits.setNthBit(index, value); + } + + @override + set length(int newLength) { + throw UnsupportedError('Cannot change the length of a fixed-length list.'); + } + + @override + Uint32List toUint32List({bool copy = true}) { + return Uint32List(1)..[0] = _bits.toInt(); + } +} + +List _createList(int length, {required bool fill}) { + final list = Uint32List(math.max(1, (length / 32).ceil())) as List; + for (var i = 0; i < list.length; i++) { + list[i] = fill ? Uint32.max : Uint32.min; + } + return list; +} + +/// A fixed-length list of bits that is stored in a fixed-length [Uint32List]. +abstract final class _Uint32ListBitList with ListBase implements BitList { + List get _bytes; + + @override + void add(bool value) { + throw UnsupportedError('Cannot add to a fixed-length list.'); + } + + @override + bool operator [](int index) { + final byteIndex = index ~/ 32; + final bitIndex = index % 32; + return _bytes[byteIndex].nthBit(bitIndex); + } + + @override + void operator []=(int index, bool value) { + final byteIndex = index ~/ 32; + final bitIndex = index % 32; + _bytes[byteIndex] = _bytes[byteIndex].setNthBit(bitIndex, value); + } + + @override + Uint32List toUint32List({bool copy = true}) { + final bytes = _bytes as Uint32List; + return copy ? Uint32List.fromList(bytes) : bytes; + } +} + +/// A fixed-length list of bits that is stored in a fixed-length [Uint32List]. +final class _FixedUint32ListBitList extends _Uint32ListBitList { + _FixedUint32ListBitList( + int length, { + bool fill = false, + }) : this._(_createList(length, fill: fill), length: length); + + _FixedUint32ListBitList._( + this._bytes, { + required this.length, + }) : assert(length > 32, 'Length must be greater than 32.'); + + @override + final List _bytes; + + @override + final int length; + + @override + set length(int newLength) { + throw UnsupportedError('Cannot change the length of a fixed-length list.'); + } +} + +/// A growable list of bits that is stored in a list of integers. +final class _GrowableUint32ListBitList extends _Uint32ListBitList { + _GrowableUint32ListBitList( + this._length, { + required bool fill, + }) : _bytes = _createList(_length, fill: fill); + + @override + int get length => _length; + int _length; + + @override + set length(int newLength) { + if (newLength < _length) { + _length = newLength; + _shrinkIfNeeded(); + } else if (newLength > _length) { + _length = newLength; + _growIfNeeded(newLength); + } + } + + @override + List _bytes; + int get _capacity => _bytes.length * 32; + + void _growIfNeeded(int index) { + if (index < _capacity) { + return; + } + final newBytes = _createList(index, fill: false); + newBytes.setAll(0, _bytes); + _bytes = newBytes; + } + + void _shrinkIfNeeded() { + final newLength = (_length / 32).ceil(); + if (newLength == _bytes.length) { + return; + } + final newBytes = Uint32List(newLength) as List; + newBytes.setRange(0, newLength, _bytes); + _bytes = newBytes; + } + + @override + void add(bool value) { + length++; + this[length - 1] = value; + } + + @override + void addAll(Iterable bits) { + if (bits is! List) { + bits = List.of(bits); + } + final newLength = length + bits.length; + length = newLength; + for (var i = 0; i < bits.length; i++) { + this[length - bits.length + i] = bits[i]; + } + } + + @override + Uint32List toUint32List({bool copy = true}) { + final bytes = _bytes as Uint32List; + if (copy) { + return bytes.sublist(0, _capacity ~/ 32); + } + return bytes.buffer.asUint32List(0, _capacity ~/ 32); + } +} diff --git a/lib/src/descriptor.dart b/lib/src/descriptor.dart index c7cd113..4f0a145 100644 --- a/lib/src/descriptor.dart +++ b/lib/src/descriptor.dart @@ -179,12 +179,14 @@ final class IntDescriptor { // OPERATIONS // --------------------------------------------------------------------------- - /// Returns an iterable of bits in [v], from least to most significant. + /// Returns a list of bits representing [v]. /// - /// The iterable has a length of [width]. + /// The list has a length of [width]. @pragma('dart2js:tryInline') @pragma('vm:prefer-inline') - Iterable bits(int v) => _BitIterable(v, width); + BitList toBitList(int v, {bool growable = false}) { + return BitList.fromInt(v, length: width, growable: growable); + } /// Returns the number of zeros in the binary representation of [v]. @pragma('dart2js:tryInline') @@ -302,14 +304,13 @@ final class IntDescriptor { @pragma('dart2js:tryInline') @pragma('vm:prefer-inline') T uncheckedChunk(int v, int left, [int? size]) { - var result = 0; - size ??= width - left; - for (var i = 0; i < size; i++) { - if (v.nthBit(left + i)) { - result |= 1 << i; - } - } - return _uncheckedCast(result); + size ??= width - left; // If size is null, use width - left + + // Calculate mask with 'size' number of 1 bits + final mask = (1 << size) - 1; + + // Shift and mask to extract the desired bits + return _uncheckedCast((v >> left) & mask); } /// Returns a new instance with bits [left] to [right], inclusive. @@ -536,63 +537,3 @@ final class IntDescriptor { return result; } } - -final class _BitIterable extends Iterable { - const _BitIterable(this._value, this.length); - final int _value; - - @override - final int length; - - @override - bool elementAt(int index) => _value.nthBit(index); - - @override - bool get first => _value.nthBit(0); - - @override - bool get last => _value.nthBit(length - 1); - - @override - bool get single { - if (length != 1) { - throw StateError('Iterable has more than one element'); - } - return _value.nthBit(0); - } - - @override - bool contains(Object? element) { - if (element is! bool) { - return false; - } - if (element) { - // Checking if at least one bit is set. - return _value != 0; - } - // At least one bit is unset. - return _value != (1 << length) - 1; - } - - @override - bool get isEmpty => _value == 0; - - @override - bool get isNotEmpty => _value != 0; - - @override - Iterator get iterator => _BitIterator(_value, length); -} - -final class _BitIterator implements Iterator { - _BitIterator(this._value, this.length); - final int _value; - final int length; - var _index = -1; - - @override - bool moveNext() => ++_index < length; - - @override - bool get current => _value.nthBit(_index); -} diff --git a/lib/src/int16.dart b/lib/src/int16.dart index 578363b..31ecf47 100644 --- a/lib/src/int16.dart +++ b/lib/src/int16.dart @@ -69,6 +69,9 @@ extension type const Int16._(int _) implements FixedInt { max: 32767, ); + /// Always `0`. + static const zero = Int16.fromUnchecked(0); + /// The minimum value that this type can represent. static const min = Int16.fromUnchecked(-32768); @@ -311,8 +314,26 @@ extension type const Int16._(int _) implements FixedInt { /// ``` Int16 midpoint(Int16 other) => Int16.fromUnchecked(_.midpoint(other._)); - /// Bits, from least significant to most significant. - Iterable get bits => _descriptor.bits(_); + /// Returns a list of bits representing this integer. + /// + /// The most significant bit is at index `0`. + /// + /// If [growable] is `true`, the returned list is a growable list. + /// + /// ## Performance + /// + /// A fixed-size list is a lightweight abstraction over a single integer, and + /// is can be inlined more easily by the compiler. Use a growable list only if + /// you need to modify the list. + /// + /// ## Example + /// + /// ```dart + /// Int16(3).bits; // [true, true, false] + /// ``` + List toBitList({bool growable = false}) { + return _descriptor.toBitList(_, growable: growable); + } /// Returns whether the n-th bit is set. bool nthBit(int n) => _.nthBit(n); diff --git a/lib/src/int32.dart b/lib/src/int32.dart index c2a3c5b..047c024 100644 --- a/lib/src/int32.dart +++ b/lib/src/int32.dart @@ -69,6 +69,9 @@ extension type const Int32._(int _) implements FixedInt { max: 2147483647, ); + /// Always `0`. + static const zero = Int32.fromUnchecked(0); + /// The minimum value that this type can represent. static const min = Int32.fromUnchecked(-2147483648); @@ -311,8 +314,26 @@ extension type const Int32._(int _) implements FixedInt { /// ``` Int32 midpoint(Int32 other) => Int32.fromUnchecked(_.midpoint(other._)); - /// Bits, from least significant to most significant. - Iterable get bits => _descriptor.bits(_); + /// Returns a list of bits representing this integer. + /// + /// The most significant bit is at index `0`. + /// + /// If [growable] is `true`, the returned list is a growable list. + /// + /// ## Performance + /// + /// A fixed-size list is a lightweight abstraction over a single integer, and + /// is can be inlined more easily by the compiler. Use a growable list only if + /// you need to modify the list. + /// + /// ## Example + /// + /// ```dart + /// Int32(3).bits; // [true, true, false] + /// ``` + List toBitList({bool growable = false}) { + return _descriptor.toBitList(_, growable: growable); + } /// Returns whether the n-th bit is set. bool nthBit(int n) => _.nthBit(n); diff --git a/lib/src/int8.dart b/lib/src/int8.dart index d62ab22..8688442 100644 --- a/lib/src/int8.dart +++ b/lib/src/int8.dart @@ -67,6 +67,9 @@ extension type const Int8._(int _) implements FixedInt { max: 127, ); + /// Always `0`. + static const zero = Int8.fromUnchecked(0); + /// The minimum value that this type can represent. static const min = Int8.fromUnchecked(-128); @@ -309,8 +312,26 @@ extension type const Int8._(int _) implements FixedInt { /// ``` Int8 midpoint(Int8 other) => Int8.fromUnchecked(_.midpoint(other._)); - /// Bits, from least significant to most significant. - Iterable get bits => _descriptor.bits(_); + /// Returns a list of bits representing this integer. + /// + /// The most significant bit is at index `0`. + /// + /// If [growable] is `true`, the returned list is a growable list. + /// + /// ## Performance + /// + /// A fixed-size list is a lightweight abstraction over a single integer, and + /// is can be inlined more easily by the compiler. Use a growable list only if + /// you need to modify the list. + /// + /// ## Example + /// + /// ```dart + /// Int8(3).bits; // [true, true, false] + /// ``` + List toBitList({bool growable = false}) { + return _descriptor.toBitList(_, growable: growable); + } /// Returns whether the n-th bit is set. bool nthBit(int n) => _.nthBit(n); diff --git a/lib/src/uint16.dart b/lib/src/uint16.dart index 3dc5f74..c376734 100644 --- a/lib/src/uint16.dart +++ b/lib/src/uint16.dart @@ -69,6 +69,9 @@ extension type const Uint16._(int _) implements FixedInt { max: 65535, ); + /// Always `0`. + static const zero = Uint16.fromUnchecked(0); + /// The minimum value that this type can represent. static const min = Uint16.fromUnchecked(0); @@ -311,8 +314,26 @@ extension type const Uint16._(int _) implements FixedInt { /// ``` Uint16 midpoint(Uint16 other) => Uint16.fromUnchecked(_.midpoint(other._)); - /// Bits, from least significant to most significant. - Iterable get bits => _descriptor.bits(_); + /// Returns a list of bits representing this integer. + /// + /// The most significant bit is at index `0`. + /// + /// If [growable] is `true`, the returned list is a growable list. + /// + /// ## Performance + /// + /// A fixed-size list is a lightweight abstraction over a single integer, and + /// is can be inlined more easily by the compiler. Use a growable list only if + /// you need to modify the list. + /// + /// ## Example + /// + /// ```dart + /// Uint16(3).bits; // [true, true, false] + /// ``` + List toBitList({bool growable = false}) { + return _descriptor.toBitList(_, growable: growable); + } /// Returns whether the n-th bit is set. bool nthBit(int n) => _.nthBit(n); diff --git a/lib/src/uint32.dart b/lib/src/uint32.dart index ce5bf70..41940d7 100644 --- a/lib/src/uint32.dart +++ b/lib/src/uint32.dart @@ -69,6 +69,9 @@ extension type const Uint32._(int _) implements FixedInt { max: 4294967295, ); + /// Always `0`. + static const zero = Uint32.fromUnchecked(0); + /// The minimum value that this type can represent. static const min = Uint32.fromUnchecked(0); @@ -311,8 +314,26 @@ extension type const Uint32._(int _) implements FixedInt { /// ``` Uint32 midpoint(Uint32 other) => Uint32.fromUnchecked(_.midpoint(other._)); - /// Bits, from least significant to most significant. - Iterable get bits => _descriptor.bits(_); + /// Returns a list of bits representing this integer. + /// + /// The most significant bit is at index `0`. + /// + /// If [growable] is `true`, the returned list is a growable list. + /// + /// ## Performance + /// + /// A fixed-size list is a lightweight abstraction over a single integer, and + /// is can be inlined more easily by the compiler. Use a growable list only if + /// you need to modify the list. + /// + /// ## Example + /// + /// ```dart + /// Uint32(3).bits; // [true, true, false] + /// ``` + List toBitList({bool growable = false}) { + return _descriptor.toBitList(_, growable: growable); + } /// Returns whether the n-th bit is set. bool nthBit(int n) => _.nthBit(n); diff --git a/lib/src/uint8.dart b/lib/src/uint8.dart index e19f908..224c1e0 100644 --- a/lib/src/uint8.dart +++ b/lib/src/uint8.dart @@ -67,6 +67,9 @@ extension type const Uint8._(int _) implements FixedInt { max: 255, ); + /// Always `0`. + static const zero = Uint8.fromUnchecked(0); + /// The minimum value that this type can represent. static const min = Uint8.fromUnchecked(0); @@ -309,8 +312,26 @@ extension type const Uint8._(int _) implements FixedInt { /// ``` Uint8 midpoint(Uint8 other) => Uint8.fromUnchecked(_.midpoint(other._)); - /// Bits, from least significant to most significant. - Iterable get bits => _descriptor.bits(_); + /// Returns a list of bits representing this integer. + /// + /// The most significant bit is at index `0`. + /// + /// If [growable] is `true`, the returned list is a growable list. + /// + /// ## Performance + /// + /// A fixed-size list is a lightweight abstraction over a single integer, and + /// is can be inlined more easily by the compiler. Use a growable list only if + /// you need to modify the list. + /// + /// ## Example + /// + /// ```dart + /// Uint8(3).bits; // [true, true, false] + /// ``` + List toBitList({bool growable = false}) { + return _descriptor.toBitList(_, growable: growable); + } /// Returns whether the n-th bit is set. bool nthBit(int n) => _.nthBit(n); diff --git a/test/bit_list_test.dart b/test/bit_list_test.dart new file mode 100644 index 0000000..13ee655 --- /dev/null +++ b/test/bit_list_test.dart @@ -0,0 +1,287 @@ +import 'package:binary/binary.dart' show BitList; + +import 'src/prelude.dart'; + +void main() { + group('fixed-length 16 bits', () { + test('fill = false', () { + final list = BitList(16); + check(list).has((a) => a.length, 'length').equals(16); + check(list).every((a) => a.isFalse()); + check(list.toUint32List()).deepEquals([0x0]); + check(list).deepEquals(BitList.fromInt(0x0, length: 16)); + }); + + test('fill = true', () { + final list = BitList(16, fill: true); + check(list).has((a) => a.length, 'length').equals(16); + check(list).every((a) => a.isTrue()); + check(list.toUint32List()).deepEquals([0xFFFFFFFF]); + check(list).deepEquals(BitList.fromInt(0xFFFFFFFF, length: 16)); + }); + + test('set bits', () { + final list = BitList(16); + for (var i = 0; i < 16; i++) { + list[i] = i.isEven; + } + check(list.toUint32List()).deepEquals([0x5555]); + check(list).deepEquals(BitList.fromInt(0x5555, length: 16)); + }); + + test('cannot add or set length', () { + final list = BitList(16); + check(() => list.add(true)).throws(); + check(() => list.length = 32).throws(); + }); + }); + + group('fixed-length 32 bits', () { + test('fill = false', () { + final list = BitList(32); + check(list).has((a) => a.length, 'length').equals(32); + check(list).every((a) => a.isFalse()); + check(list.toUint32List()).deepEquals([0x0]); + check(list).deepEquals(BitList.fromInt(0x0)); + }); + + test('fill = true', () { + final list = BitList(32, fill: true); + check(list).has((a) => a.length, 'length').equals(32); + check(list).every((a) => a.isTrue()); + check(list.toUint32List()).deepEquals([0xFFFFFFFF]); + check(list).deepEquals(BitList.fromInt(0xFFFFFFFF)); + }); + + test('set bits', () { + final list = BitList(32); + for (var i = 0; i < 32; i++) { + list[i] = i.isEven; + } + check(list.toUint32List()).deepEquals([0x55555555]); + check(list).deepEquals(BitList.fromInt(0x55555555)); + }); + + test('cannot add or set length', () { + final list = BitList(32); + check(() => list.add(true)).throws(); + check(() => list.length = 64).throws(); + }); + }); + + group('fixed-length 64 bits', () { + test('fill = false', () { + final list = BitList(64); + check(list).has((a) => a.length, 'length').equals(64); + check(list).every((a) => a.isFalse()); + check(list.toUint32List()).deepEquals([0x0, 0x0]); + }); + + test('fill = true', () { + final list = BitList(64, fill: true); + check(list).has((a) => a.length, 'length').equals(64); + check(list).every((a) => a.isTrue()); + check(list.toUint32List()).deepEquals([0xFFFFFFFF, 0xFFFFFFFF]); + }); + + test('set bits', () { + final list = BitList(64); + for (var i = 0; i < 64; i++) { + list[i] = i.isEven; + } + check(list.toUint32List()).deepEquals([0x55555555, 0x55555555]); + check(list.toUint32List(copy: false)).deepEquals( + [0x55555555, 0x55555555], + ); + + final init = list.toUint32List(copy: false); + for (var i = 0; i < 64; i++) { + list[i] = i.isOdd; + } + check(init).deepEquals([0xAAAAAAAA, 0xAAAAAAAA]); + }); + + test('cannot add or set length', () { + final list = BitList(64); + check(() => list.add(true)).throws(); + check(() => list.length = 128).throws(); + }); + }); + + group('growable 16 bits', () { + test('fill = false', () { + final list = BitList(16, growable: true); + check(list).has((a) => a.length, 'length').equals(16); + check(list).every((a) => a.isFalse()); + check(list.toUint32List()).deepEquals([0x0]); + check(list).deepEquals(BitList.fromInt(0x0, length: 16)); + }); + + test('fill = true', () { + final list = BitList(16, growable: true, fill: true); + check(list).has((a) => a.length, 'length').equals(16); + check(list).every((a) => a.isTrue()); + check(list.toUint32List()).deepEquals([0xFFFFFFFF]); + check(list).deepEquals(BitList.fromInt(0xFFFFFFFF, length: 16)); + }); + + test('set bits', () { + final list = BitList(16, growable: true); + for (var i = 0; i < 16; i++) { + list[i] = i.isEven; + } + check(list.toUint32List()).deepEquals([0x5555]); + check(list).deepEquals(BitList.fromInt(0x5555, length: 16)); + }); + + test('add bits', () { + final list = BitList(0, growable: true); + check(list).has((a) => a.length, 'length').equals(0); + + for (var i = 0; i < 16; i++) { + list.add(i.isEven); + } + check(list).has((a) => a.length, 'length').equals(16); + check(list.toUint32List()).deepEquals([0x5555]); + + for (var i = 0; i < 16; i++) { + list.add(i.isOdd); + } + check(list).has((a) => a.length, 'length').equals(32); + check(list.toUint32List()).deepEquals([0xAAAA5555]); + }); + + test('set length', () { + final list = BitList(16, growable: true); + list.length = 32; + check(list).has((a) => a.length, 'length').equals(32); + check(list).every((a) => a.isFalse()); + check(list.toUint32List()).deepEquals([0x0]); + check(list).deepEquals(BitList.fromInt(0x0)); + }); + }); + + group('growable 32 bits', () { + test('fill = false', () { + final list = BitList(32, growable: true); + check(list).has((a) => a.length, 'length').equals(32); + check(list).every((a) => a.isFalse()); + check(list.toUint32List()).deepEquals([0x0]); + check(list).deepEquals(BitList.fromInt(0x0)); + }); + + test('fill = true', () { + final list = BitList(32, growable: true, fill: true); + check(list).has((a) => a.length, 'length').equals(32); + check(list).every((a) => a.isTrue()); + check(list.toUint32List()).deepEquals([0xFFFFFFFF]); + check(list).deepEquals(BitList.fromInt(0xFFFFFFFF)); + }); + + test('set bits', () { + final list = BitList(32, growable: true); + for (var i = 0; i < 32; i++) { + list[i] = i.isEven; + } + check(list.toUint32List()).deepEquals([0x55555555]); + check(list).deepEquals(BitList.fromInt(0x55555555)); + }); + + test('add bits', () { + final list = BitList(0, growable: true); + check(list).has((a) => a.length, 'length').equals(0); + + for (var i = 0; i < 32; i++) { + list.add(i.isEven); + } + check(list).has((a) => a.length, 'length').equals(32); + check(list.toUint32List()).deepEquals([0x55555555]); + + for (var i = 0; i < 32; i++) { + list.add(true); + } + check(list).has((a) => a.length, 'length').equals(64); + check(list.toUint32List()).deepEquals([0x55555555, 0xFFFFFFFF]); + }); + + test('set length', () { + final list = BitList(32, growable: true); + list.length = 64; + check(list).has((a) => a.length, 'length').equals(64); + check(list).every((a) => a.isFalse()); + check(list.toUint32List()).deepEquals([0x0, 0x0]); + }); + + test('shrink length', () { + final list = BitList(32, growable: true); + list.length = 64; + list.length = 32; + check(list).has((a) => a.length, 'length').equals(32); + check(list).every((a) => a.isFalse()); + check(list.toUint32List()).deepEquals([0x0]); + check(list).deepEquals(BitList.fromInt(0x0)); + }); + + test('insert bits', () { + final list = BitList(31, growable: true); + list.insert(0, true); + check(list).has((a) => a.length, 'length').equals(32); + check(list.toUint32List()).deepEquals([0x1]); + + list.insertAll(0, List.generate(32, (i) => i.isEven)); + check(list).has((a) => a.length, 'length').equals(64); + check(list.toUint32List()).deepEquals([0x55555555, 0x1]); + }); + + test('addAll', () { + final list = BitList(32, growable: true); + list.addAll(Iterable.generate(32, (i) => i.isEven)); + check(list).has((a) => a.length, 'length').equals(64); + check(list.toUint32List()).deepEquals([0x0, 0x55555555]); + }); + + test('toUint32List(copy: false)', () { + final list = BitList(32, growable: true); + list.addAll(Iterable.generate(32, (i) => i.isEven)); + final uint32List = list.toUint32List(copy: false); + check(uint32List).deepEquals([0x0, 0x55555555]); + + list.add(true); + check(uint32List).deepEquals([0x0, 0x55555555]); + }); + }); + + test('BitList.fromInt growable: true', () { + final list = BitList.fromInt(0x55555555, growable: true); + check(list).has((a) => a.length, 'length').equals(32); + check(list.toUint32List()).deepEquals([0x55555555]); + }); + + group('BitList.from', () { + test('fixed 32-bit', () { + final list = BitList.from(Iterable.generate(32, (_) => true)); + check(list).has((a) => a.length, 'length').equals(32); + check(list.toUint32List()).deepEquals([0xFFFFFFFF]); + }); + + test('fixed 64-bit', () { + final list = BitList.from(Iterable.generate(64, (_) => true)); + check(list).has((a) => a.length, 'length').equals(64); + check(list.toUint32List()).deepEquals([0xFFFFFFFF, 0xFFFFFFFF]); + }); + + test('growable 128-bit', () { + final list = BitList.from( + Iterable.generate(128, (_) => true), + growable: true, + ); + check(list).has((a) => a.length, 'length').equals(128); + check(list.toUint32List()).deepEquals([ + 0xFFFFFFFF, + 0xFFFFFFFF, + 0xFFFFFFFF, + 0xFFFFFFFF, + ]); + }); + }); +} diff --git a/test/collect_bytes_test.dart b/test/collect_bytes_test.dart new file mode 100644 index 0000000..b8ceb70 --- /dev/null +++ b/test/collect_bytes_test.dart @@ -0,0 +1,32 @@ +import 'package:binary/binary.dart' show collectBytes; +import 'package:test/test.dart'; + +import 'src/prelude.dart'; + +void main() { + test('collect bytes', () async { + final (bytes, _) = collectBytes( + Stream.fromIterable([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]), + ); + await check(bytes).completes((b) { + b.deepEquals([1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + }); + + test('cancel collect bytes', () async { + final (bytes, cancel) = collectBytes( + Stream.fromIterable([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]), + ); + await cancel(); + await pumpEventQueue(); + check(bytes).doesNotComplete(); + }); +} diff --git a/test/int_descriptor_test.dart b/test/int_descriptor_test.dart index aa17614..8495afe 100644 --- a/test/int_descriptor_test.dart +++ b/test/int_descriptor_test.dart @@ -41,9 +41,9 @@ void _test(IntDescriptor i) { check(set).equals(0); }); - test('bit iterable', () { + test('bit list', () { final bits = int.parse('10101010', radix: 2); - final it = i.bits(bits); + final it = i.toBitList(bits); check(it.elementAt(0)).equals(false); check(it.elementAt(1)).equals(true); @@ -70,7 +70,7 @@ void _test(IntDescriptor i) { max: 1, ); final bits = int.parse('1', radix: 2); - final it = i1.bits(bits); + final it = i1.toBitList(bits); check(it.single).equals(true); }); diff --git a/test/signed_int_test.dart b/test/signed_int_test.dart index e0cda3d..fe1603a 100644 --- a/test/signed_int_test.dart +++ b/test/signed_int_test.dart @@ -12,6 +12,20 @@ void main() { debugCheckFixedWithInRange = true; }); + test('isValid, checkRange', () { + check(Int8.isValid(0)).isTrue(); + check(Int8.isValid(127)).isTrue(); + check(Int8.isValid(-128)).isTrue(); + check(Int8.isValid(-129)).isFalse(); + check(Int8.isValid(128)).isFalse(); + + check(() => Int8.checkRange(0)).returnsNormally(); + check(() => Int8.checkRange(127)).returnsNormally(); + check(() => Int8.checkRange(-128)).returnsNormally(); + check(() => Int8.checkRange(-129)).throws(); + check(() => Int8.checkRange(128)).throws(); + }); + test('Int8.min is -128', () { check(Int8.min).equals(Int8(-128)); }); @@ -177,7 +191,7 @@ void main() { group('individual bit operations', () { test('bits iterator', () { final i = int.parse('10101010', radix: 2).toSigned(8); - final bits = Int8(i).bits; + final bits = Int8(i).toBitList(); check(bits).deepEquals([ false, true, diff --git a/test/unsigned_int_test.dart b/test/unsigned_int_test.dart index 3f765a8..150efee 100644 --- a/test/unsigned_int_test.dart +++ b/test/unsigned_int_test.dart @@ -12,6 +12,18 @@ void main() { debugCheckFixedWithInRange = true; }); + test('isValid, checkRange', () { + check(Uint8.isValid(0)).isTrue(); + check(Uint8.isValid(255)).isTrue(); + check(Uint8.isValid(-1)).isFalse(); + check(Uint8.isValid(256)).isFalse(); + + check(() => Uint8.checkRange(0)).returnsNormally(); + check(() => Uint8.checkRange(255)).returnsNormally(); + check(() => Uint8.checkRange(-1)).throws(); + check(() => Uint8.checkRange(256)).throws(); + }); + test('Uint8.min is 0', () { check(Uint8.min).equals(Uint8(0)); }); @@ -164,7 +176,7 @@ void main() { group('individual bit operations', () { test('bits iterator', () { final i = int.parse('10101010', radix: 2); - final bits = Uint8(i).bits; + final bits = Uint8(i).toBitList(); check(bits).deepEquals([ false, true, diff --git a/tool/template b/tool/template index 8c3f2a1..adc9cb4 100644 --- a/tool/template +++ b/tool/template @@ -67,6 +67,12 @@ extension type const {{NAME}}._(int _) implements FixedInt { max: {{MAX}}, ); + /// Always `0`. + static const zero = {{NAME}}.fromUnchecked(0); + + /// Always `1`. + static const one = {{NAME}}.fromUnchecked(1); + /// The minimum value that this type can represent. static const min = {{NAME}}.fromUnchecked({{MIN}}); @@ -309,8 +315,26 @@ extension type const {{NAME}}._(int _) implements FixedInt { /// ``` {{NAME}} midpoint({{NAME}} other) => {{NAME}}.fromUnchecked(_.midpoint(other._)); - /// Bits, from least significant to most significant. - Iterable get bits => _descriptor.bits(_); + /// Returns a list of bits representing this integer. + /// + /// The most significant bit is at index `0`. + /// + /// If [growable] is `true`, the returned list is a growable list. + /// + /// ## Performance + /// + /// A fixed-size list is a lightweight abstraction over a single integer, and + /// is can be inlined more easily by the compiler. Use a growable list only if + /// you need to modify the list. + /// + /// ## Example + /// + /// ```dart + /// {{NAME}}(3).bits; // [true, true, false] + /// ``` + List toBitList({bool growable = false}) { + return _descriptor.toBitList(_, growable: growable); + } /// Returns whether the n-th bit is set. bool nthBit(int n) => _.nthBit(n);