Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In ruby reverse string without affecting numbers in a string and don't use regex [closed]

Tags:

ruby

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.

like image 697
Harishbn Avatar asked Dec 18 '22 12:12

Harishbn


2 Answers

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"       
like image 73
Cary Swoveland Avatar answered Jun 05 '23 13:06

Cary Swoveland


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"]
like image 42
iGian Avatar answered Jun 05 '23 13:06

iGian