String#byteslice
str.byteslice(index) or str.byteslice(start, length) The byteslice method extracts a portion of a string based on byte positions rather than character positions. This is essential when working with binary data, specific byte-level operations, or when you need precise control over multi-byte character encodings like UTF-8.
Unlike slice, which works with character indices, byteslice operates at the byte level. This distinction becomes critical when working with non-ASCII characters where a single character may occupy multiple bytes.
The method is especially useful when the bytes themselves matter more than the characters they form. That happens in protocol parsing, binary file handling, and other low-level tasks where a character boundary is not the right unit of work.
Because it works in bytes, byteslice gives you deterministic offsets even when the string contains multi-byte characters. That is valuable when the surrounding system measures size in bytes, not in glyphs or user-visible characters.
Syntax
str.byteslice(index) # Single byte position
str.byteslice(start, length) # Range of bytes
str.byteslice(range) # Byte range
Parameters
index— A zero-based byte position (Integer)start— The starting byte position (Integer)length— The number of bytes to extract (Integer)range— A Range of byte positions
Return Value
Returns a new string containing the extracted bytes, or nil if the index is out of bounds.
Examples
Basic byte extraction
ascii = "hello"
ascii.byteslice(0) # => "h"
ascii.byteslice(1) # => "e"
ascii.byteslice(-1) # => "o"
A single integer argument returns one byte as a one-character string. Negative indices count backward from the end, just as they do with String#[] and Array#[], so -1 always gives you the last byte regardless of the string length.
Extracting a byte range
"hello".byteslice(1, 3)
# => "ell"
"hello".byteslice(1..3)
# => "ell"
In the ASCII range, byteslice and slice behave identically because each character is exactly one byte. This makes byteslice a drop-in replacement for simple Latin text, but the difference becomes important as soon as the content includes characters outside the ASCII set, where a single visible character can span multiple bytes.
Working with multi-byte characters
When dealing with UTF-8 encoded strings, characters beyond ASCII occupy multiple bytes:
text = "こんにちは" # 5 characters, 15 bytes in UTF-8
text.byteslice(0) # Returns nil because first character starts at byte 0 but is 3 bytes long
text.byteslice(0, 3) # => "こ" - first 3 bytes form the first character
text.byteslice(3, 3) # => "ん" - bytes 3-5 form the second character
With multi-byte encodings, knowing the byte length of each character is essential for using byteslice correctly. In UTF-8, most Japanese characters occupy three bytes each, so advancing by multiples of three keeps the slices aligned. If you slice at an offset that falls in the middle of a character, byteslice returns nil rather than producing a broken byte sequence.
Comparing byteslice vs slice
text = "日本"
text.slice(0) # => "日" - character at position 0
text.byteslice(0) # => nil - cannot extract single byte from 3-byte character
text.slice(0, 1) # => "日" - 1 character
text.byteslice(0, 1) # => nil - returns nil for partial character
text.byteslice(0, 3) # => "日" - 3 bytes = 1 complete character
The key takeaway from the comparison is that byteslice returns nil when the requested range splits a multi-byte character. It does not fall back to returning partial or garbled text — you get the complete character or nothing. This all-or-nothing behaviour is intentional and keeps byte-level access safe to use without accidentally producing invalid UTF-8 sequences.
common use cases
Extract bytes from binary data
data = "\xFF\xFE\x00\x01"
data.byteslice(0, 2) # => "\xFF\xFE"
data.byteslice(2, 2) # => "\x00\x01"
Binary data has no concept of characters, so byteslice is the natural tool for extracting fixed-width fields from a byte sequence. The start and length arguments map directly to the offsets you would see in a hex dump or a protocol specification, which keeps the code aligned with the data format rather than with Ruby’s character model.
Parse byte-level protocols
def extract_header(bytes)
{
version: bytes.byteslice(0, 1).ord,
flags: bytes.byteslice(1, 1).ord,
length: bytes.byteslice(2, 2).unpack("n").first
}
end
This is the kind of task where byteslice really shines: when you know the exact byte offsets of the fields you need, you can pull them out without worrying about character boundaries or encoding mismatches. The method stays predictable because it counts raw bytes, which is exactly what a binary protocol specification describes.
Handle encoding edge cases
def truncate_to_bytes(str, max_bytes)
return "" if max_bytes <= 0
str.byteslice(0, max_bytes) || str
end
truncate_to_bytes("こんにちは", 6)
# => "こん" - cuts at byte boundary, not character
This byte-level truncation is useful when storage or transport has a byte limit and you do not want Ruby to guess at a character-level split for you. The tradeoff is that you may still need to think about encoding if the resulting string is shown back to a person.
error handling
byteslice returns nil when the index is out of bounds, making it safe to use without raising errors:
"hi".byteslice(10) # => nil
"hi".byteslice(-10) # => nil
"".byteslice(0) # => nil
However, the two-argument form behaves the same way when given invalid arguments: a negative length or a start position beyond the string both produce nil rather than raising. This consistency makes error handling predictable across all three calling conventions:
"hello".byteslice(0, -1) # => nil (negative length)
In practice, the nil return value is convenient because you can treat out-of-range access as a missing slice instead of an exception. That makes byteslice a good fit for defensive parsing code where out-of-bounds access should be handled gracefully rather than by rescuing an error, and where you would rather check for nil than wrap every access in a begin/rescue block.
performance
byteslice is efficient because it creates a new substring from existing bytes without re-encoding:
text = "hello world"
text.byteslice(0, 5) # => "hello" - no re-encoding needed
See Also
- String#slice - Extracts a substring by character position
- String#bytesize - Returns the byte count of a string
- String#chars - Returns an array of characters