Given a string containing letters and digits I wish to return a string that contains the letters rearranged without affecting the positions of the digits. The ith letter in the string is to become the ith-from-last letter in the returned string. A regular expression cannot be used.
Example: if the given string were
"hello123wor63ld"
the string
"dlrow123oll63eh"
should be returned.
I found a solution using a regex but cannot figure out how to solve the problem without using a regex.
Here are three ways to get the job done. None mutate the original string.
DIGITS = '0'..'9'
str = "hello123wor63ld"
#1 Single pass with swapping
def doit(str)
s = str.dup
ifirst = -1
ilast = str.size
loop do
ifirst = (ifirst+1..ilast-2).find { |i| !DIGITS.cover?(s[i]) }
break if ifirst.nil?
ilast = (ilast-1).downto(ifirst+1).find { |i| !DIGITS.cover?(s[i]) }
break if ilast.nil?
s[ifirst], s[ilast] = s[ilast], s[ifirst]
end
s
end
doit(str)
#=> "dlrow123oll63eh"
I list this method first because it is the most efficient, requiring a single pass through the string and a constant amount of memory beyond that used to store the string that is returned.
#2 Remove digits, reverse, insert digits
str.delete('0123456789').reverse.tap do |s|
str.each_char.with_index { |c,i| s.insert(i,c) if DIGITS.cover?(c) }
end
#=> "dlrow123oll63eh"
The steps are as follows.
s = str.delete('0123456789').reverse
#=> dlrowolleh
Note String#delete does not mutate its receiver.
Continuing, in Object#tap's block,
s #=> dlrowolleh
Then,
enum0 = str.each_char
#=> #<Enumerator: "hello123wor63ld":each_char>
enum1 = enum0.with_index
#=> #<Enumerator: #<Enumerator: "hello123wor63ld":each_char>:with_index>
The first element is generated by enum1
and passed to the block, where the block variables are assigned values by the process of Array decomposition.
c,i = enum1.next #=> ["h", 0]
c #=> "h"
i #=> 0
The block calculation is then performed.
DIGITS.cover?(c) #=> false
so
s.insert(i,c)
is not executed (s
remains unchanged). Similarly, no characters are inserted for the next four elements generated and passed to the block by enum1
.
c,i = enum1.next #=> ["e", 1]
DIGITS.cover?(c) #=> false
c,i = enum1.next #=> ["l", 2]
DIGITS.cover?(c) #=> false
c,i = enum1.next #=> ["l", 3]
DIGITS.cover?(c) #=> false
c,i = enum1.next #=> ["o", 4]
DIGITS.cover?(c) #=> false
s #=> "dlrowolleh"
Now, however,
c,i = enum1.next #=> ["1", 5]
DIGITS.cover?(c) #=> true
so
s.insert(i,c) #=> "dlrow1olleh"
is executed to insert "1"
after "w"
. See
String#insert.
The remaining calculations are similar.
#2 Save indices of non-digits in and array non_digit_idx
then map each characters to itself if it is a digit and to non_digit_idx
if it is a non-digit.
non_digit_idx = str.each_char.with_index.with_object([]) do |(c,i),a|
a << i unless DIGITS.cover?(c)
end
str.each_char.map.with_index do |c,i|
DIGITS.cover?(c) ? str[i] : str[non_digit_idx.pop]
end.join
#=> "dlrow123oll63eh"
The steps are as follows.
non_digit_idx = str.each_char.with_index.with_object([]) do |(c,i),a|
a << i unless rng.cover?(c)
end
#=> [0, 1, 2, 3, 4, 8, 9, 10, 13, 14]
This constructs an array of the indices of characters that are not digits. See Range#cover?.
"hello123wor63ld"
0 1
01234 890 34
Next,
a = str.each_char.map.with_index do |c,i|
rng.cover?(c) ? str[i] : str[non_digit_idx.pop]
end
#=> ["d", "l", "r", "o", "w", "1", "2", "3", "o", "l", "l", "6", "3", "e", "h"]
Here I map each character of str
to itself if it is a digit, and to the character that is at index non_digit_idx.pop
if it is not a digit.
Lastly, join the characters in the mapped array.
a.join
#=> "dlrow123oll63eh"
This is one way with no regex at all:
input = "hello123wor63ld"
output = "dlrow123oll63eh"
res = input.chars.map{ |ch| ch.to_i.to_s == ch ? ch.to_i : ch }.then do |ary|
chars = ary.reject{ |e| e.is_a? Integer }.reverse
ary.map { |e| e.is_a?(Integer) ? e : chars.shift }
end.join
res == output #=> true
Splitting the logic.
The first step converts digits to integers:
input.chars.map{ |ch| ch.to_i.to_s == ch ? ch.to_i : ch }
#=> ["h", "e", "l", "l", "o", 1, 2, 3, "w", "o", "r", 6, 3, "l", "d"]
The second step reverses only the chars, not the digits:
input.chars.map{ |ch| ch.to_i.to_s == ch ? ch.to_i : ch }
.then { |ary| ary.reject{ |e| e.is_a? Integer }.reverse }
#=> ["d", "l", "r", "o", "w", "o", "l", "l", "e", "h"]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With