0

Let's say I have the following string abcixigea and I want to replace the first 'i', 'e' and the second 'a' with '1', '3' and '4', getting all the combinations with those "progressive" replacement.

So, I need to get:
abc1xigea
abcixig3a
abcixig34
abc1xige4
...and so on.

I tried with itertools.product following this question python string replacement, all possible combinations #2 but the result I get is not exactly what I need and I can see why.
However I'm stuck on trying with combinations and keeping parts of the string fixed (changing just some chars as explained above).

overstack
  • 3
  • 2
  • when you say "the first" occurrence, are you scanning from left to right? in your example, why do you replace the second 'a'? – THK Jun 04 '17 at 01:16
  • Sorry, I forgot to mention that I want to replace just the second 'a'. – overstack Jun 04 '17 at 01:20

1 Answers1

1
from itertools import product

s = "abc{}xig{}{}"

for combo in product(("i", 1), ("e", 3), ("a", 4)):
    print(s.format(*combo))

produces

abcixigea
abcixige4
abcixig3a
abcixig34
abc1xigea
abc1xige4
abc1xig3a
abc1xig34

Edit: in a more general way, you want something like:

from itertools import product

def find_nth(s, char, n):
    """
    Return the offset of the nth occurrence of char in s,
      or -1 on failure
    """
    assert len(char) == 1
    offs = -1
    for _ in range(n):
        offs = s.find(char, offs + 1)
        if offs == -1:
            break
    return offs

def gen_replacements(base_string, *replacement_values):
    """
    Generate all string combinations from base_string
      by replacing some characters according to replacement_values

    Each replacement_value is a tuple of
      (original_char, occurrence, replacement_char)
    """
    assert len(replacement_values) > 0
    # find location of each character to be replaced
    replacement_offsets = [
        (find_nth(base_string, orig, occ), orig, occ, (orig, repl))
        for orig,occ,repl in replacement_values
    ]
    # put them in ascending order
    replacement_offsets.sort()
    # make sure all replacements are actually possible
    if replacement_offsets[0][0] == -1:
        raise ValueError("'{}' occurs less than {} times".format(replacement_offsets[0][1], replacement_offsets[0][2]))
    # create format string and argument list
    args = []
    for i, (offs, _, _, arg) in enumerate(replacement_offsets):
        # we are replacing one char with two, so we have to
        # increase the offset of each replacement by
        # the number of replacements already made
        base_string = base_string[:offs + i] + "{}" + base_string[offs + i + 1:]
        args.append(arg)
    # ... and we feed that into the original code from above:
    for combo in product(*args):
        yield base_string.format(*combo)

def main():
    s = "abcixigea"

    for result in gen_replacements(s, ("i", 1, "1"), ("e", 1, "3"), ("a", 2, "4")):
        print(result)

if __name__ == "__main__":
    main()

which produces exactly the same output as above.

Hugh Bothwell
  • 55,315
  • 8
  • 84
  • 99
  • Nice, thanks! But if I try with a number of {} greater than the tuple length, I get the error 'IndexError: tuple index out of range'. I would like a solution that applies to every generic case, not just tied to my example. – overstack Jun 04 '17 at 02:13
  • I understood better what it does and why that error. I had to add additional tuples in the main tuple to match all the {} and it works just fine. Even if it's not that elegant, it gets the job done. Alternative solutions are just welcome :) – overstack Jun 04 '17 at 02:42
  • "Not that elegant"? Do feel free to produce a "more elegant" version before criticizing. – Hugh Bothwell Jun 04 '17 at 02:58
  • Oh no, I wasn't criticizing your approach, it is elegant! I was talking about my adding additional tuples to match the number of {} instead of expand your solution (i.e. as you did in your edit). Thanks again! – overstack Jun 04 '17 at 03:08