Hamza
Hamza

Reputation: 6025

How to 'swap' words (multiple characters) in a string?

Consider the following examples:

string_now = 'apple and avocado'
stringthen = string_now.swap('apple', 'avocado') # stringthen = 'avocado and apple'

and:

string_now = 'fffffeeeeeddffee'
stringthen = string_now.swap('fffff', 'eeeee') # stringthen = 'eeeeefffffddffee'

Approaches discussed in Swap character of string in Python do not work, as the mapping technique used there only takes one character into consideration. Python's built-in str.maketrans() also only supports one-character translations, as when I try to do multiple characters, it throws the following error:

Traceback (most recent call last):
  File "main.py", line 4, in <module>
    s.maketrans(mapper)
ValueError: string keys in translate table must be of length 1

A chain of replace() methods is not only far from ideal (since I have many replacements to do, chaining replaces would be a big chunk of code) but because of its sequential nature, it will not translate things perfectly as:

string_now = 'apple and avocado'
stringthen = string_now.replace('apple', 'avocado').replace('avocado', 'apple')

gives 'apple and apple' instead of 'avocado and apple'.

What's the best way to achieve this?

Upvotes: 10

Views: 10803

Answers (5)

Stefan_EOX
Stefan_EOX

Reputation: 1529

This solution uses str.format():

string_now = "apple and avocado"
stringthen = (  # "avocado and apple"
    string_now.replace("apple", "{apple}")
    .replace("avocado", "{avocado}")
    .format(apple="avocado", avocado="apple")
)

# Edit: as a function
def swap_words(s, x, y):
    return s.replace(x, "{" + x + "}")
            .replace(y, "{" + y + "}")
            .format(**{x: y, y: x})

It first adds curly brackets before and after keywords to turn them into placeholders. Then str.format() is used to replace the placeholders.

Upvotes: 3

Kingname
Kingname

Reputation: 1362

Why not just use a temp string which will never be in the origin string?

for example:

>>> a = 'apples and avocados and avocados and apples'
>>> b = a.replace('apples', '#IamYourFather#').replace('avocados', 'apples').replace('#IamYourFather#', 'avocados')
>>> print(b)
avocados and apples and apples and avocados

where #IamYourFather# is a string which will never be in the origin string.

Upvotes: 2

Kelly Bundy
Kelly Bundy

Reputation: 27588

Two regex solutions and one for other people who do have a character that can't appear (there are over a million different possible characters, after all) and who don't dislike replace chains :-)

def swap_words_regex1(s, x, y):
    return re.sub(re.escape(x) + '|' + re.escape(y),
                  lambda m: (x if m[0] == y else y),
                  s)

def swap_words_regex2(s, x, y):
    return re.sub(f'({re.escape(x)})|{re.escape(y)}',
                  lambda m: x if m[1] is None else y,
                  s)

def swap_words_replaces(s, x, y):
    return s.replace(x, chr(0)).replace(y, x).replace(chr(0), y)

Some benchmark results:

 3.7 ms  1966 kB  swap_words_split
10.7 ms  2121 kB  swap_words_regex1
17.8 ms  2121 kB  swap_words_regex2
 1.3 ms   890 kB  swap_words_replaces

Full code (Try it online!):

from timeit import repeat
import re
import tracemalloc as tm

def swap_words_split(s, x, y):
    return y.join(part.replace(y, x) for part in s.split(x))

def swap_words_regex1(s, x, y):
    return re.sub(re.escape(x) + '|' + re.escape(y),
                  lambda m: (x if m[0] == y else y),
                  s)

def swap_words_regex2(s, x, y):
    return re.sub(f'({re.escape(x)})|{re.escape(y)}',
                  lambda m: x if m[1] is None else y,
                  s)

def swap_words_replaces(s, x, y):
    return s.replace(x, chr(0)).replace(y, x).replace(chr(0), y)

funcs = swap_words_split, swap_words_regex1, swap_words_regex2, swap_words_replaces

args = 'apples and avocados and bananas and oranges and ' * 10000, 'apples', 'avocados'

for _ in range(3):
    for func in funcs:
        t = min(repeat(lambda: func(*args), number=1))
        tm.start()
        func(*args)
        memory = tm.get_traced_memory()[1]
        tm.stop()
        print(f'{t * 1e3:4.1f} ms  {memory // 1000:4} kB  {func.__name__}')
    print()

Upvotes: 9

Karl Knechtel
Karl Knechtel

Reputation: 61478

Given that we want to swap words x and y, and that we don't care about the situation where they overlap, we can:

  • split the string on occurrences of x
  • within each piece, replace y with x
  • join the pieces with y

Essentially, we use split points within the string as a temporary marker to avoid the problem with sequential replacements.

Thus:

def swap_words(s, x, y):
    return y.join(part.replace(y, x) for part in s.split(x))

Test it:

>>> swap_words('apples and avocados and avocados and apples', 'apples', 'avocados')
'avocados and apples and apples and avocados'
>>>

Upvotes: 17

notnull
notnull

Reputation: 94

I managed to make this function that does exactly what you want.

def swapwords(mystr, firstword, secondword):
    splitstr = mystr.split(" ")

    for i in range(len(splitstr)):
        if splitstr[i] == firstword:
            splitstr[i] = secondword
            i+=1
        if splitstr[i] == secondword:
            splitstr[i] = firstword
            i+=1

    newstr = " ".join(splitstr)

   return newstr

Basically, what this does is it takes in your string "Apples and Avacados", and splits it by spaces. Thus, each word gets indexed in an array splitstr[]. Using this, we can use a for loop to swap the words. The i+=1 is in order to ensure the words don't get swapped twice. Lastly, I join the string back using newstr= " ".join(splitstr) which joins the words separated by a space.

Running the following code gives us: Avacados and Apples.

Upvotes: -2

Related Questions