Generating a Succession of Strings
Problem
You want to iterate over a series of alphabetically-increasing strings as you would over a series of numbers.
Solution
If you know both the start and end points of your succession, you can simply create a range and use Range#each, as you would for numbers:
('aa'..'ag').each { |x| puts x } # aa # ab # ac # ad # ae # af # ag
The method that generates the successor of a given string is String#succ. If you don't know the end point of your succession, you can define a generator that uses succ, and break from the generator when you're done.
def endless_string_succession(start) while true yield start start = start.succ end end
This code iterates over an endless succession of strings, stopping when the last two letters are the same:
endless_string_succession('fol') do |x| puts x break if x[-1] == x[-2] end # fol # fom # fon # foo
Discussion
Imagine a string as an odometer. Each character position of the string has a separate dial, and the current odometer reading is your string. Each dial always shows the same kind of character. A dial that starts out showing a number will always show a number. A character that starts out showing an uppercase letter will always show an uppercase letter.
The string succession operation increments the odometer. It moves the rightmost dial forward one space. This might make the rightmost dial wrap around to the beginning: if that happens, the dial directly to its left is also moved forward one space. This might make that dial wrap around to the beginning, and so on:
'89999'.succ # => "90000" 'nzzzz'.succ # => "oaaaa"
When the leftmost dial wraps around, a new dial is added to the left of the odometer. The new dial is always of the same type as the old leftmost dial. If the old leftmost dial showed capital letters, then so will the new leftmost dial:
'Zzz'.succ # => "AAaa"
Lowercase letters wrap around from "z" to "a". If the first character is a lowercase letter, then when it wraps around, an "a" is added on to the beginning of the string:
'z'.succ # => "aa" 'aa'.succ # => "ab" 'zz'.succ # => "aaa"
Uppercase letters work in the same way: "Z" becomes "A". Lowercase and uppercase letters never mix.
'AA'.succ # => "AB" 'AZ'.succ # => "BA" 'ZZ'.succ # => "AAA" 'aZ'.succ # => "bA" 'Zz'.succ # => "AAa"
Digits in a string are treated as numbers, and wrap around from 9 to 0, just like a car odometer.
'foo19'.succ # => "foo20" 'foo99'.succ # => "fop00" '99'.succ # => "100" '9Z99'.succ # => "10A00"
Characters other than alphanumerics are not incremented unless they are the only characters in the string. They are simply ignored when calculating the succession, and reproduced in the same positions in the new string. This lets you build formatting into the strings you want to increment.
'10-99'.succ # => "11-00"
When nonalphanumerics are the only characters in the string, they are incremented according to ASCII order. Eventually an alphanumeric will show up, and the rules for strings containing alphanumerics will take over.
'a-a'.succ # => "a-b" 'z-z'.succ # => "aa-a" 'Hello!'.succ # => "Hellp!" %q{'zz'}.succ # => "'aaa'" %q{z'zz'}.succ # => "aa'aa'" '$$$$'.succ # => "$$$%" s = '!@-' 13.times { puts s = s.succ } # !@. # !@/ # !@0 # !@1 # !@2 # … # !@8 # !@9 # !@10
There's no reverse version of String#succ. Matz, and the community as a whole, think there's not enough demand for such a method to justify the work necessary to handle all the edge cases. If you need to iterate over a succession of strings in reverse, your best bet is to transform the range into an array and iterate over that in reverse:
("a".."e").to_a.reverse_each { |x| puts x } # e # d # c # b # a
See Also
- Recipe 2.15, "Generating a Sequence of Numbers"
- Recipe 3.4, "Iterating Over Dates"