Skip to content

Commit f2ee1b4

Browse files
hasumikinandrykonchin
authored andcommitted
[Feature #22118] Introduce Basic Bit Operations into String (#17353)
This patch adds the following methods to String class: * String#bit_get(offset, lsb_first: true) -> 1 | 0 | nil * String#bit_set?(offset, lsb_first: true) -> true | false | nil * String#bit_set(offset, lsb_first: true) -> self * String#bit_clear(offset, lsb_first: true) -> self * String#bit_flip(offset, lsb_first: true) -> self * String#bit_count -> Integer * String#bitwise_not -> String * String#bitwise_not! -> self * String#bitwise_and(other) -> String * String#bitwise_and!(other) -> self * String#bitwise_or(other) -> String * String#bitwise_or!(other) -> self * String#bitwise_xor(other) -> String * String#bitwise_xor!(other) -> self Other than implementation, tests, specs, and docs are added. Link: [Feature #22118] ## Note In `string.c`, I wrote some big macro that create method functions and helper functions: * STR_DEFINE_BINARY_BITWISE_METHOD * STR_DEFINE_UNARY_BITWISE_KERNEL * STR_DEFINE_BINARY_BITWISE_KERNEL While using macros like this reduces maintainability, I believe it's acceptable because there are no plans to extend the `bitwise_*` methods beyond this proposal, and the logic is stable. On the other hand, other methods such as `bit_get` and `bit_count` are planned to have argument extensions in the future.
1 parent 6dde411 commit f2ee1b4

10 files changed

Lines changed: 349 additions & 0 deletions

core/string/bit_clear_spec.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bit_clear" do
6+
it "clears a bit in LSB-first order by default and returns self" do
7+
str = +"\xFF"
8+
str.bit_clear(1).should.equal?(str)
9+
str.should == "\xFD"
10+
end
11+
12+
it "clears a bit in MSB-first order" do
13+
str = +"\xFF"
14+
str.bit_clear(1, lsb_first: false)
15+
str.should == "\xBF"
16+
end
17+
18+
it "preserves byte order when using MSB-first order" do
19+
str = +"\xFF\xFF"
20+
str.bit_clear(8, lsb_first: false)
21+
str.should == "\xFF\x7F"
22+
end
23+
24+
it "raises an IndexError for an out of range bit offset" do
25+
-> { "\x00".bit_clear(8) }.should.raise(IndexError)
26+
-> { "\x00".bit_clear(-1) }.should.raise(IndexError)
27+
end
28+
29+
it "raises a FrozenError if self is frozen" do
30+
-> { "\x00".freeze.bit_clear(0) }.should.raise(FrozenError)
31+
end
32+
end
33+
end

core/string/bit_count_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bit_count" do
6+
it "returns the number of set bits in the string" do
7+
"".bit_count.should == 0
8+
"\x00".bit_count.should == 0
9+
"\xFF".bit_count.should == 8
10+
"\xAA\xF0".bit_count.should == 8
11+
end
12+
13+
it "raises an ArgumentError when given an argument" do
14+
-> { "\x00".bit_count(0) }.should.raise(ArgumentError)
15+
-> { "\x00".bit_count(lsb_first: false) }.should.raise(ArgumentError)
16+
end
17+
end
18+
end

core/string/bit_flip_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bit_flip" do
6+
it "flips a bit in LSB-first order by default and returns self" do
7+
str = +"\x00"
8+
str.bit_flip(1).should.equal?(str)
9+
str.should == "\x02"
10+
str.bit_flip(1)
11+
str.should == "\x00"
12+
end
13+
14+
it "flips a bit in MSB-first order" do
15+
str = +"\x00"
16+
str.bit_flip(1, lsb_first: false)
17+
str.should == "\x40"
18+
end
19+
20+
it "preserves byte order when using MSB-first order" do
21+
str = +"\x00\x00"
22+
str.bit_flip(8, lsb_first: false)
23+
str.should == "\x00\x80"
24+
end
25+
26+
it "raises an IndexError for an out of range bit offset" do
27+
-> { "\x00".bit_flip(8) }.should.raise(IndexError)
28+
-> { "\x00".bit_flip(-1) }.should.raise(IndexError)
29+
end
30+
31+
it "raises a FrozenError if self is frozen" do
32+
-> { "\x00".freeze.bit_flip(0) }.should.raise(FrozenError)
33+
end
34+
end
35+
end

core/string/bit_get_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bit_get" do
6+
it "returns 0 or 1 for a bit offset in LSB-first order by default" do
7+
str = "\xAA"
8+
str.bit_get(0).should == 0
9+
str.bit_get(1).should == 1
10+
str.bit_get(7).should == 1
11+
end
12+
13+
it "returns 0 or 1 for a bit offset in MSB-first order" do
14+
str = "\xAA"
15+
str.bit_get(0, lsb_first: false).should == 1
16+
str.bit_get(1, lsb_first: false).should == 0
17+
str.bit_get(7, lsb_first: false).should == 0
18+
end
19+
20+
it "preserves byte order when using MSB-first order" do
21+
str = "\x00\x80"
22+
str.bit_get(8, lsb_first: false).should == 1
23+
end
24+
25+
it "returns nil for a bit offset beyond the string" do
26+
"\x00".bit_get(8).should == nil
27+
"".bit_get(0).should == nil
28+
end
29+
30+
it "raises an IndexError for a negative bit offset" do
31+
-> { "\x00".bit_get(-1) }.should.raise(IndexError)
32+
end
33+
34+
it "raises an ArgumentError for an invalid lsb_first value" do
35+
-> { "\x00".bit_get(0, lsb_first: nil) }.should.raise(ArgumentError)
36+
end
37+
end
38+
end

core/string/bit_set_p_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bit_set?" do
6+
it "returns true or false for a bit offset in LSB-first order by default" do
7+
str = "\xAA"
8+
str.bit_set?(0).should == false
9+
str.bit_set?(1).should == true
10+
str.bit_set?(7).should == true
11+
end
12+
13+
it "returns true or false for a bit offset in MSB-first order" do
14+
str = "\xAA"
15+
str.bit_set?(0, lsb_first: false).should == true
16+
str.bit_set?(1, lsb_first: false).should == false
17+
str.bit_set?(7, lsb_first: false).should == false
18+
end
19+
20+
it "preserves byte order when using MSB-first order" do
21+
str = "\x00\x80"
22+
str.bit_set?(8, lsb_first: false).should == true
23+
end
24+
25+
it "returns nil for a bit offset beyond the string" do
26+
"\x00".bit_set?(8).should == nil
27+
"".bit_set?(0).should == nil
28+
end
29+
30+
it "raises an IndexError for a negative bit offset" do
31+
-> { "\x00".bit_set?(-1) }.should.raise(IndexError)
32+
end
33+
34+
it "raises an ArgumentError for an invalid lsb_first value" do
35+
-> { "\x00".bit_set?(0, lsb_first: nil) }.should.raise(ArgumentError)
36+
end
37+
end
38+
end

core/string/bit_set_spec.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bit_set" do
6+
it "sets a bit in LSB-first order by default and returns self" do
7+
str = +"\x00"
8+
str.bit_set(1).should.equal?(str)
9+
str.should == "\x02"
10+
end
11+
12+
it "sets a bit in MSB-first order" do
13+
str = +"\x00"
14+
str.bit_set(1, lsb_first: false)
15+
str.should == "\x40"
16+
end
17+
18+
it "preserves byte order when using MSB-first order" do
19+
str = +"\x00\x00"
20+
str.bit_set(8, lsb_first: false)
21+
str.should == "\x00\x80"
22+
end
23+
24+
it "raises an IndexError for an out of range bit offset" do
25+
-> { "\x00".bit_set(8) }.should.raise(IndexError)
26+
-> { "\x00".bit_set(-1) }.should.raise(IndexError)
27+
end
28+
29+
it "raises a FrozenError if self is frozen" do
30+
-> { "\x00".freeze.bit_set(0) }.should.raise(FrozenError)
31+
end
32+
end
33+
end

core/string/bitwise_and_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bitwise_and" do
6+
it "returns a new string containing the byte-wise AND with another string" do
7+
str = "\xF0"
8+
result = str.bitwise_and("\xCC")
9+
result.should == "\xC0".b
10+
result.should_not.equal?(str)
11+
str.should == "\xF0"
12+
end
13+
14+
it "converts the argument with to_str" do
15+
other = mock("string")
16+
other.should_receive(:to_str).and_return("\xCC")
17+
"\xF0".bitwise_and(other).should == "\xC0".b
18+
end
19+
20+
it "raises an ArgumentError if byte sizes differ" do
21+
-> { "\xF0".bitwise_and("") }.should.raise(ArgumentError)
22+
-> { "\xF0".bitwise_and("\x00\x00") }.should.raise(ArgumentError)
23+
end
24+
25+
it "returns a BINARY string" do
26+
(+"\xF0").force_encoding("UTF-8").bitwise_and("\xCC").encoding.should == Encoding::BINARY
27+
end
28+
end
29+
30+
describe "String#bitwise_and!" do
31+
it "replaces self with the byte-wise AND and returns self" do
32+
str = +"\xF0"
33+
str.bitwise_and!("\xCC").should.equal?(str)
34+
str.should == "\xC0"
35+
end
36+
37+
it "raises a FrozenError if self is frozen" do
38+
-> { "\x00".freeze.bitwise_and!("\x00") }.should.raise(FrozenError)
39+
end
40+
end
41+
end

core/string/bitwise_not_spec.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bitwise_not" do
6+
it "returns a new string with every bit inverted" do
7+
str = "\x00\xAA"
8+
result = str.bitwise_not
9+
result.should == "\xFF\x55".b
10+
result.should_not.equal?(str)
11+
str.should == "\x00\xAA"
12+
end
13+
14+
it "returns a BINARY string" do
15+
str = (+"\x00").force_encoding("US-ASCII")
16+
str.bitwise_not.encoding.should == Encoding::BINARY
17+
end
18+
end
19+
20+
describe "String#bitwise_not!" do
21+
it "inverts every bit in self and returns self" do
22+
str = +"\x00\xAA"
23+
str.bitwise_not!.should.equal?(str)
24+
str.should == "\xFF\x55"
25+
end
26+
27+
it "raises a FrozenError if self is frozen" do
28+
-> { "\x00".freeze.bitwise_not! }.should.raise(FrozenError)
29+
end
30+
end
31+
end

core/string/bitwise_or_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bitwise_or" do
6+
it "returns a new string containing the byte-wise OR with another string" do
7+
str = "\xF0"
8+
result = str.bitwise_or("\x0C")
9+
result.should == "\xFC".b
10+
result.should_not.equal?(str)
11+
str.should == "\xF0"
12+
end
13+
14+
it "converts the argument with to_str" do
15+
other = mock("string")
16+
other.should_receive(:to_str).and_return("\x0C")
17+
"\xF0".bitwise_or(other).should == "\xFC".b
18+
end
19+
20+
it "raises an ArgumentError if byte sizes differ" do
21+
-> { "\xF0".bitwise_or("") }.should.raise(ArgumentError)
22+
-> { "\xF0".bitwise_or("\x00\x00") }.should.raise(ArgumentError)
23+
end
24+
25+
it "returns a BINARY string" do
26+
(+"\xF0").force_encoding("UTF-8").bitwise_or("\x0C").encoding.should == Encoding::BINARY
27+
end
28+
end
29+
30+
describe "String#bitwise_or!" do
31+
it "replaces self with the byte-wise OR and returns self" do
32+
str = +"\xF0"
33+
str.bitwise_or!("\x0C").should.equal?(str)
34+
str.should == "\xFC"
35+
end
36+
37+
it "raises a FrozenError if self is frozen" do
38+
-> { "\x00".freeze.bitwise_or!("\x00") }.should.raise(FrozenError)
39+
end
40+
end
41+
end

core/string/bitwise_xor_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# encoding: binary
2+
require_relative '../../spec_helper'
3+
4+
ruby_version_is "4.1" do
5+
describe "String#bitwise_xor" do
6+
it "returns a new string containing the byte-wise XOR with another string" do
7+
str = "\xF0"
8+
result = str.bitwise_xor("\xCC")
9+
result.should == "\x3C".b
10+
result.should_not.equal?(str)
11+
str.should == "\xF0"
12+
end
13+
14+
it "converts the argument with to_str" do
15+
other = mock("string")
16+
other.should_receive(:to_str).and_return("\xCC")
17+
"\xF0".bitwise_xor(other).should == "\x3C".b
18+
end
19+
20+
it "raises an ArgumentError if byte sizes differ" do
21+
-> { "\xF0".bitwise_xor("") }.should.raise(ArgumentError)
22+
-> { "\xF0".bitwise_xor("\x00\x00") }.should.raise(ArgumentError)
23+
end
24+
25+
it "returns a BINARY string" do
26+
(+"\xF0").force_encoding("UTF-8").bitwise_xor("\xCC").encoding.should == Encoding::BINARY
27+
end
28+
end
29+
30+
describe "String#bitwise_xor!" do
31+
it "replaces self with the byte-wise XOR and returns self" do
32+
str = +"\xF0"
33+
str.bitwise_xor!("\xCC").should.equal?(str)
34+
str.should == "\x3C"
35+
end
36+
37+
it "raises a FrozenError if self is frozen" do
38+
-> { "\x00".freeze.bitwise_xor!("\x00") }.should.raise(FrozenError)
39+
end
40+
end
41+
end

0 commit comments

Comments
 (0)