The Ruby String class includes the "next" function - also called "succ" for "successor" - which advances a string to the next logical item in, usually, an alphanumeric progression. For example, "9" goes to "10" goes to "11"; or "a" goes to "b" goes to "c". There is no opposite call in the standard String class. Searcing on the Web reveals that conventional wisdom regards this as difficult! It's certainly impossible to cover every case, since both "09".next and "9".next return "10", so going backwards from "10", it isn't possible to know whether to return "09" or "9".
Despite such difficulties, it's not actually that hard to make something that covers the vast majority of cases if you accept that it can't ever be perfect. The code below makes a very good job of returning, using "prev", to a string upon which "next" had been called. Extensive comments describe the implementation and its limitations.
class String
def prev(collapse = false)
str = self.dup
early_exit = false
any_done = false
ranges = [
('0'[0]..'9'[0]),
('a'[0]..'z'[0]),
('A'[0]..'Z'[0]),
nil
]
first_ranged = nil
for index in (1..str.length)
byte = str[index - 1]
within = ranges.select do |range|
range.nil? or range.include?(byte)
end [0]
unless within.nil?
case within.first
when '0'[0]
match_byte = '1'[0]
else
match_byte = within.first
end
first_ranged = index - 1 if (byte == match_byte)
first_within = within
break
end
end
for index in (1..str.length)
byte = str[-index]
within = ranges.select do |range|
range.nil? or range.include?(byte)
end [0]
next if within.nil?
any_done = true
byte = byte - 1
if (within.include? byte)
str[-index] = byte
early_exit = true
break
else
str[-index] = within.last
if (first_ranged != nil and first_within.include?(byte + 1) and (first_ranged - str.length) == -(index + 1))
str.slice!(-(index + 1))
early_exit = true
break
end
end
end
if (any_done == true and early_exit == false)
return nil
else
return str
end
end
def prev!
new_str = prev
self.replace(new_str) unless new_str.nil?
return self
end
alias pred prev
alias pred! prev!
end
To prove it does an OK job, try some test code:
def run_tests(iterations = 1000)
puts "\n"
puts "Running tests...\n"
iterations.times do
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
@newpass = ""
len = rand(16)+1
1.upto(len) { |i| @newpass << chars[rand(chars.size-1)] }
next if @newpass.prev.nil?
@passcp1 = @newpass.dup; @passcp1.prev!
@passcp2 = @newpass.dup; @passcp2.pred!
if (
@newpass != @newpass.next.prev or @newpass != @newpass.prev.next or
@newpass != @newpass.succ.pred or @newpass != @newpass.pred.succ or
@newpass.prev != @passcp1 or
@newpass.pred != @passcp2
)
puts "Failed on '#{@newpass}'\n"
puts " with --> '#{@newpass}' vs '#{@newpass.next.prev}' vs '#{@newpass.prev.next}'\n"
puts " or --> '#{@newpass.prev}' vs '#{@passcp1}'\n"
puts " or --> '#{@newpass.pred}' vs '#{@passcp2}'\n"
raise "Tests failed!"
end
end
puts "Tests completed successfully.\n"
end
run_tests(10000)