Issue #22118 has been updated by mame (Yusuke Endoh). There's no writer that takes the bit value as an argument. Is it really OK? We need to write `str.send(val == 1 ? :bit_set : :bit_clear, i)` to do so, which is clearly painful. Could we have something like `bit_put(offset, value)`? (If `bit_put` is available, we don't need `bit_set`, `bit_clear`, and `bit_flip`, IMO.) ---------------------------------------- Feature #22118: Introduce Basic Bit Operations into String https://bugs.ruby-lang.org/issues/22118#change-118033 * Author: hasumikin (hitoshi hasumi) * Status: Open ---------------------------------------- This PR implements basic bit operations into String, incorporating revisions made following the discussion (esp. #note-5) PR URL: https://github.com/ruby/ruby/pull/17353 ## Single-bit read and mutation * `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` **Note for `bit_set`/`bit_clear`/`bit_flip`:** As these methods are destructive-only (they have no non-destructive counterpart), no `!` is added. The `!` suffix is meant to distinguish the dangerous member of a destructive/non-destructive pair, following the `setbyte` precedent. ## Bitwise operators * `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` ## Popcount * `String#bit_count -> Integer` --- ## API Contracts ### 1. Bit-order keyword #### `lsb_first: true` (default) Within each byte, `offset = 0` is the LSB. Numbering proceeds upward through byte[0] and then continues at the LSB of byte[1]: ```text byte[0] byte[1] offset: 7 6 5 4 3 2 1 0 15 14 13 12 11 10 9 8 bit: b7 b6 b5 b4 b3 b2 b1 b0 b7 b6 b5 b4 b3 b2 b1 b0 ^ ^ LSB LSB offset = byte_index * 8 + bit_in_byte ``` #### `lsb_first: false` for MSB-first Byte order is preserved (`byte[0]` is still first), but within each byte numbering starts at the MSB: ```text byte[0] byte[1] offset: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bit: b7 b6 b5 b4 b3 b2 b1 b0 b7 b6 b5 b4 b3 b2 b1 b0 ^ ^ MSB MSB offset = byte_index * 8 + (7 - bit_in_byte) ``` The keyword only controls bit numbering within each byte. Byte order itself is unchanged. LSB-first is the more common convention for general in-memory bitmap use, including formats and APIs such as Apache Arrow validity bitmaps, ext4 block bitmaps, Roaring bitmaps, Linux/BSD bitmap APIs, and hardware register descriptions. MSB-first is also needed for domains such as RFC-style network protocol diagrams, PNG low-bit-depth scanlines, BitTorrent bitfields, and some compressed bit streams. This is the reason that we define `lsb_first: true` as the default. The bit-order keyword must be accepted by all `bit_*` methods with the same default; a getter and a setter disagreeing on bit addressing would be disastrous. **One temporary exception:** Since the `bit_count` introduced in this version counts the population of the entire string without taking arguments, it does not accept a bit-order keyword. If arguments for specifying a range are introduced in the future, the bit-order keyword should be introduced at that time. ### 2. Out-of-range behavior Read methods: `String#bit_get` and `String#bit_set?` return `nil` when the bit offset is out of range. On the other hand, mutating methods such as `String#bit_set`, `String#bit_clear`, and `String#bit_flip` raise `IndexError` for an out-of-range bit offset. This follows the same general distinction as read-like access versus write-like access: reading a missing position can return `nil`, but mutating a missing position should not silently do nothing. If the bit offset is too large to be represented internally, all of these methods raise `ArgumentError` before applying the read/write out-of-range rule. This bound is specified explicitly because the representable offset range may differ between Ruby implementations. ### 3. Negative bit offsets Negative bit offsets are rejected with `IndexError`. Although Ruby often uses negative indices to count from the end, combining negative bit offsets with the `lsb_first: true/false` mechanism would make the numbering rule harder to explain and reason about. The API keeps bit offsets as non-negative flat positions from the beginning of the string. ### 4. Length mismatch for binary bitwise operations `String#bitwise_and`, `String#bitwise_or`, `String#bitwise_xor`, and their `!` variants require both operands to have the same byte size. If the byte sizes differ, they raise `ArgumentError`. This avoids implicit truncation or zero-padding. Since these methods operate on strings as fixed-size bitmaps, requiring equal sizes makes the operation explicit and predictable. Silent zero-extension would let corrupted results flow downstream in the typical use cases (checksums, masks); callers who want padding can `ljust` explicitly. ### 5. Encoding All bit operations treat the receiver as a byte sequence. For non-destructive methods that return a String, the return value's encoding is set to `Encoding::BINARY`. Destructive methods do not alter the receiver's encoding, consistent with `setbyte`, which likewise lets a byte-level mutation produce an encoding-invalid string without changing the encoding. -- https://bugs.ruby-lang.org/