Skip to content

Commit 73f65aa

Browse files
committed
Allow dumping and loading bitarrays
1 parent 7bc7388 commit 73f65aa

File tree

6 files changed

+205
-1
lines changed

6 files changed

+205
-1
lines changed

README.md

+20
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,28 @@ ba = BitArray.new(16, ["0000111111110000"].pack('B*'), reverse_byte: false)
6161
ba.to_s # "0000111111110000"
6262
```
6363

64+
Saving and loading `BitArray`:
65+
66+
```ruby
67+
ba = BitArray.new(16, ["0000111111110000"].pack('B*'))
68+
ba.dump(File.new("bitarray.dat", "w"))
69+
#=> #<File:bitarray.dat>
70+
ba = BitArray.load(File.open("bitarray.dat"))
71+
ba.to_s # "1111000000001111"
72+
```
73+
74+
Read-only access without loading it into memory:
75+
76+
```ruby
77+
ba = BitArray.new(16, ["0000111111110000"].pack('B*'))
78+
ba.dump(File.new("bitarray.dat", "w"))
79+
ba_ro = BitArrayFile.new(filename: "bitarray.dat")
80+
ba_ro[0] # 1
81+
ba_ro[4] # 0
82+
```
6483

6584
## History
85+
- 1.4 in 2022 (cleanups, add unions, dump/load, and BitArrayFile)
6686
- 1.3 in 2022 (cleanups and a minor perf tweak)
6787
- 1.2 in 2018 (Added option to skip reverse the bits for each byte by @dalibor)
6888
- 1.1 in 2018 (fixed a significant bug)

lib/bitarray.rb

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
require_relative "bitarray/bit_array"
2+
require_relative "bitarray/bit_array_file"

lib/bitarray/bit_array.rb

+36-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ class BitArray
22
attr_reader :field, :reverse_byte, :size
33
include Enumerable
44

5-
VERSION = "1.3.0"
5+
VERSION = "1.4.0"
6+
HEADER_LENGTH = 8 + 1 # QC (@size, @reverse_byte)
67

78
def initialize(size, field = nil, reverse_byte: true)
89
@size = size
@@ -24,6 +25,26 @@ def [](position)
2425
(@field.getbyte(position >> 3) & (1 << (byte_position(position) % 8))) > 0 ? 1 : 0
2526
end
2627

28+
def ==(rhs)
29+
@size == rhs.size && @reverse_byte == rhs.reverse_byte && @field == rhs.field
30+
end
31+
32+
# Allows joining (union) two bitarrays of identical size.
33+
# The resulting bitarray will contain any bit set in either constituent arrays.
34+
# |= is implicitly defined, so you can do source_ba |= other_ba
35+
def |(rhs)
36+
raise ArgumentError.new("Bitarray sizes must be identical") if @size != rhs.size
37+
raise ArgumentError.new("Reverse byte settings must be identical") if @reverse_byte != rhs.reverse_byte
38+
39+
combined = BitArray.new(@size, @field, reverse_byte: @reverse_byte)
40+
rhs.field.each_byte.inject(0) do |byte_pos, byte|
41+
combined.field.setbyte(byte_pos, combined.field.getbyte(byte_pos) | byte)
42+
byte_pos + 1
43+
end
44+
45+
combined
46+
end
47+
2748
# Iterate over each bit
2849
def each
2950
return to_enum(:each) unless block_given?
@@ -55,4 +76,18 @@ def total_set
5576
private def byte_position(position)
5677
@reverse_byte ? position : 7 - position
5778
end
79+
80+
# Save contents to an io device such as a file
81+
def dump(io)
82+
io.write([@size, @reverse_byte ? 1 : 0].pack("QC"))
83+
io.write(@field.b)
84+
io
85+
end
86+
87+
# Load bitarray from an io device such as a file
88+
def self.load(io)
89+
size, reverse_byte = io.read(9).unpack("QC")
90+
field = io.read
91+
new(size, field, reverse_byte: reverse_byte == 1)
92+
end
5893
end

lib/bitarray/bit_array_file.rb

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require_relative "bit_array"
2+
3+
# Read-only access to a BitArray dumped to disk.
4+
# This is considerably slower than using the RAM-based BitArray, but
5+
# avoids the memory requirements and initial setup time.
6+
class BitArrayFile
7+
HEADER_LENGTH = BitArray::HEADER_LENGTH
8+
9+
attr_reader :io, :reverse_byte, :size
10+
11+
def initialize(filename: nil, io: nil)
12+
if io
13+
@io = io
14+
elsif filename
15+
@io = File.open(filename, "r")
16+
else
17+
raise ArgumentError.new("Must specify a filename or io argument")
18+
end
19+
20+
@io.seek(0)
21+
@size, @reverse_byte = @io.read(9).unpack("QC")
22+
@reverse_byte = @reverse_byte != 0
23+
end
24+
25+
# Read a bit (1/0)
26+
def [](position)
27+
seek_to(position >> 3)
28+
(@io.getbyte & (1 << (byte_position(position) % 8))) > 0 ? 1 : 0
29+
end
30+
31+
private def byte_position(position)
32+
@reverse_byte ? position : 7 - position
33+
end
34+
35+
private def seek_to(position)
36+
@io.seek(position + HEADER_LENGTH)
37+
end
38+
end

test/test_bit_array.rb

+45
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "minitest/autorun"
2+
require "tempfile"
23
require_relative "../lib/bitarray"
34

45
class TestBitArray < Minitest::Test
@@ -79,6 +80,50 @@ def test_total_set
7980
ba[9] = 1
8081
assert_equal 3, ba.total_set
8182
end
83+
84+
def test_dump_load
85+
ba_dump = BitArray.new(35)
86+
[1, 5, 6, 7, 10, 16, 33].each { |i| ba_dump[i] = 1}
87+
Tempfile.create("bit_array.dat") do |io|
88+
ba_dump.dump(io)
89+
io.rewind
90+
ba_load = BitArray.load(io)
91+
92+
assert_equal ba_dump, ba_load
93+
end
94+
end
95+
96+
def test_union
97+
set_bits = [1, 5, 6, 7, 10, 16, 33].shuffle
98+
99+
ba_lhs = BitArray.new(35)
100+
set_bits[0..3].each { |i| ba_lhs[i] = 1}
101+
ba_rhs = BitArray.new(35)
102+
# Deliberately overlap a little
103+
set_bits[3..-1].each { |i| ba_rhs[i] = 1}
104+
ba_expected = BitArray.new(35)
105+
set_bits.each { |i| ba_expected[i] = 1}
106+
107+
assert_equal ba_lhs | ba_rhs, ba_expected
108+
end
109+
110+
def test_union_unequal_sizes
111+
ba_lhs = BitArray.new(4)
112+
ba_rhs = BitArray.new(5)
113+
114+
assert_raises ArgumentError do
115+
ba_lhs | ba_rhs
116+
end
117+
end
118+
119+
def test_union_unequal_reverse_bytes
120+
ba_lhs = BitArray.new(4, reverse_byte: true)
121+
ba_rhs = BitArray.new(4, reverse_byte: false)
122+
123+
assert_raises ArgumentError do
124+
ba_lhs | ba_rhs
125+
end
126+
end
82127
end
83128

84129
class TestBitArrayWhenNonReversedByte < Minitest::Test

test/test_bit_array_file.rb

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
require "minitest/autorun"
2+
require "tempfile"
3+
require_relative "../lib/bitarray"
4+
5+
class TestBitArrayFile < Minitest::Test
6+
def setup
7+
ba = BitArray.new(35)
8+
[1, 5, 6, 7, 10, 16, 33].each { |i| ba[i] = 1}
9+
@file = Tempfile.new("bit_array_file.dat")
10+
ba.dump(@file)
11+
@file.rewind
12+
end
13+
14+
def teardown
15+
@file.close
16+
@file.unlink
17+
end
18+
19+
def test_from_filename
20+
baf = BitArrayFile.new(filename: @file.path)
21+
for i in 0...35
22+
expected = [1, 5, 6, 7, 10, 16, 33].include?(i) ? 1 : 0
23+
assert_equal expected, baf[i]
24+
end
25+
end
26+
27+
def test_from_io
28+
baf = BitArrayFile.new(io: @file)
29+
for i in 0...35
30+
expected = [1, 5, 6, 7, 10, 16, 33].include?(i) ? 1 : 0
31+
assert_equal expected, baf[i]
32+
end
33+
end
34+
end
35+
36+
class TestBitArrayFileWhenNonReversedByte < Minitest::Test
37+
def setup
38+
ba = BitArray.new(35, reverse_byte: false)
39+
[1, 5, 6, 7, 10, 16, 33].each { |i| ba[i] = 1}
40+
@file = Tempfile.new("bit_array_file.dat")
41+
ba.dump(@file)
42+
@file.rewind
43+
end
44+
45+
def teardown
46+
@file.close
47+
@file.unlink
48+
end
49+
50+
def test_from_filename
51+
baf = BitArrayFile.new(filename: @file.path)
52+
for i in 0...35
53+
expected = [1, 5, 6, 7, 10, 16, 33].include?(i) ? 1 : 0
54+
assert_equal expected, baf[i]
55+
end
56+
end
57+
58+
def test_from_io
59+
baf = BitArrayFile.new(io: @file)
60+
for i in 0...35
61+
expected = [1, 5, 6, 7, 10, 16, 33].include?(i) ? 1 : 0
62+
assert_equal expected, baf[i]
63+
end
64+
end
65+
end

0 commit comments

Comments
 (0)