rubyguides

String#succ

Signature

str.succ   -> new_string
str.succ!  -> self
str.next   -> new_string
str.next!  -> self

#succ returns a new string. #succ! mutates self in place. #next and #next! are aliases that call the identical C implementation.

Numeric Increment

Digits increment with carry propagation, working from right to left. When a 9 rolls over, it resets to 0 and increments the digit to its left by one. If every digit in the string carries — as with '99' — a new leading digit 1 is prepended, which is why the result is '100' rather than '00'.

'0'.succ        # => "1"
'8'.succ        # => "9"
'9'.succ        # => "10"
'00'.succ       # => "01"
'09'.succ       # => "10"
'99'.succ       # => "100"
'1999zzz'.succ  # => "2000aaa"
'0099'.succ     # => "0100"

Leading zeros are preserved during the increment, which makes succ useful for generating zero-padded identifiers like invoice numbers or serial codes. The carry still works correctly: '0099' becomes '0100', keeping the same width. This behaviour is consistent for any number of leading zeros.

Lowercase Letters

Lowercase letters follow the same carry logic, preserving case.

'a'.succ        # => "b"
'x'.succ        # => "y"
'z'.succ        # => "aa"
'aa'.succ       # => "ab"
'az'.succ       # => "ba"
'zz'.succ       # => "aaa"

'z' rolls over to 'aa', not to an uppercase letter — the carry stays within the same character class. This rule holds for every rollover in succ: digits stay digits, lowercase stays lowercase, and uppercase stays uppercase. The method treats each character class as an independent counting system that wraps around within its own boundaries.

Uppercase Letters

Same behavior as lowercase, but preserves uppercase.

'A'.succ        # => "B"
'Z'.succ        # => "AA"
'AA'.succ       # => "AB"
'AZ'.succ       # => "BA"
'ZZ'.succ       # => "AAA"

Uppercase letters follow the same carry logic as lowercase letters, but the two cases are kept strictly separate. A string of uppercase Zs rolls over to uppercase As, and a string of lowercase zs rolls over to lowercase as. The method never crosses case boundaries during a carry, which is important when generating identifiers where case distinguishes one value from another. Keeping case consistent means you can rely on the output matching the input’s character class.

Non-Alphanumeric Characters

Non-alphanumerics use the underlying character set’s collating sequence. For ASCII/BINARY encodings, this is byte value order. Non-alphanumerics do not trigger carry propagation — only the rightmost character is ever incremented.

'***'.succ           # => "**+"
'<<koala>>'.succ     # => "<<koalb>>"

* (ASCII 42) increments to + (ASCII 43). Unlike alphanumeric characters, non-alphanumeric symbols do not trigger carry propagation — only the last character in the string changes. This means a string of asterisks like '***'.succ returns '**+' rather than something like '+**', because the carry stops at the rightmost position.

Multibyte Characters

With multibyte encodings like UTF-8, each character is treated as a codepoint. The increment follows the encoding’s collating sequence.

'å'.succ        # => "å" (no next codepoint, wraps to same — depends on encoding)

Behavior varies by Ruby version and encoding. For encodings with no defined successor (or where the last codepoint rolls over), the result may be the same character or require explicit handling.

Mixed alphanumeric strings

Carry propagates across and between types. Letters roll over independently of digits.

'THX1138'.succ   # => "THX1139"
'zz99zz99'.succ  # => "aaa00aa00"
'99zz99zz'.succ  # => "100aa00aa"
'ZZZ9999'.succ   # => "AAAA0000"

'ZZZ9999'.succ produces 'AAAA0000' because every character type rolls over independently — letters wrap within their case and digits wrap within 0-9. No character type crosses into another, so a digit never becomes a letter and vice versa. This separation makes the method predictable for structured identifiers that mix numbers and text.

Empty Strings

Returns a new empty string.

''.succ  # => ""

Binary Strings

Each byte is treated as an independent numeric value ranging from \x00 to \xFF. A full-byte carry from \xFF wraps around to \x00 and prepends \x01 to the front of the string, just as decimal 99 wraps to 100. This byte-level behaviour is consistent with how succ handles digits and letters, but the result is meaningful only in an ASCII-8BIT context. For general binary data, this property is rarely useful outside of low-level protocol or encoding work.

"\x00\x00\x00".succ       # => "\x00\x00\x01"
"\xFF\xFF\xFF".succ       # => "\x01\x00\x00\x00"  (ASCII-8BIT encoding)

Each byte in the binary string is treated as an independent 8-bit value, so \xFF (255) wraps around to \x00 and carries to the next byte to the left. This byte-level increment is consistent with how succ handles digits and letters, but the result is only meaningful in an ASCII-8BIT context. For arbitrary binary data, this property is rarely useful outside of low-level protocol work.

Frozen Strings

#succ returns a new string and is safe to use on frozen strings. #succ! raises FrozenError on frozen strings in Ruby 3.0+.

s = 'hello'.freeze
s.succ   # => "hellp"     # safe, returns new string

s.succ!  # => FrozenError (Ruby 3.0+)

Using succ on a frozen string is safe because it returns a new object without modifying the original. The bang variant succ! attempts to modify the receiver in place, which fails on frozen strings. This distinction matters in performance-sensitive code where you want to avoid allocating new strings on every iteration.

Gotchas

No numeric awareness. Successor is purely character-based.

'1.0'.succ   # => "1.1", not "2.0"

Mixed-type carry cascades can surprise you. 'ZZZ9999'.succ produces 'AAAA0000' — every character type rolls over independently.

#next conflicts with the next keyword. Inside blocks using next for flow control, prefer #succ to avoid ambiguity.

Non-ASCII encodings follow the encoding’s collating sequence, which may not match locale expectations.

#pred does not exist for strings. Ruby has Integer#pred but not String#pred. The successor operation is not reversible through a built-in method, so if you need to walk backward through a sequence of identifiers, you must track the previous value yourself or generate the sequence in advance.

See Also

  • String#hex — converts a hex string to an integer
  • String#oct — converts an octal string to an integer