Home > OS >  Randomly replacing letters, numbers, and punctuation in a string: can this code be condensed?
Randomly replacing letters, numbers, and punctuation in a string: can this code be condensed?

Time:11-11

Writing a function to check an input string for numbers, and if there are any, to randomize every digit, letter, and punctuation mark in the string. (i.e. "hello3.14" might become "jdbme6?21")

This code works (and the goal makes sense in context, I promise) but it sure seems redundant. Not sure how to tighten it up. The ELSE is just there to make me feel better about loose ends, but it's probably disposable.

My primary question is, Can this method be condensed? Secondary question, Is there a completely different, better way I should do this? Thanks for any guidance.

import random
import string


def new_thing(old_thing):
    output_str = ''
    if any(char.isdigit() for char in old_thing):
        for char in old_thing:
            get_new = char
            if char in string.digits:
                while get_new == char:
                    get_new = random.choice(string.digits)
                output_str  = get_new
            elif char in string.ascii_lowercase:
                while get_new == char:
                    get_new = random.choice(string.ascii_lowercase)
                output_str  = get_new
            elif char in string.punctuation:
                while get_new == char:
                    get_new = random.choice(string.punctuation)
                output_str  = get_new
            else:
                output_str  = char
        print(output_str)
    else:
        print("lol no numbers gg")


new_thing(input("Type a thing: ").lower())

CodePudding user response:

As pointed out, this isn't really the place for reviewing code, but since it's here I wanted to point out how to do your selections without needing a while loop.

A while loop will work, but has a real downside, in that it's no longer a consistent time to finish. It also has a theoretical downside in that it has no theoretical guarantee of ever succeeding, but you're not likely to actually hit that problem.

I'll use digits as an example. In all cases, you'll need the index of the character.

char_idx = string.digits.find(char)

One way is to make a new string of only available characters.

avail_digits = string.digits[:char_idx] string.digits[char_idx 1:]
get_new = random.choice(avail_digits)

Another is to select a random index from the length of the string minus one, and add that to your current index. Use the modulus operator % to wrap around to the beginning. By selecting one less than the string length, you will never wrap around to your original character.

get_new_idx = random.randrange(0, len(string.digits)-1) % len(string.digits)
get_new = string.digits[get_new_idx]

You might find it simpler to consider the current index as a marker between a lower and upper half of the string. You still select an index one less than the length of the string, but if the new index is the current index of higher, add one to shift it into the range of the upper part.

get_new_idx = random.randrange(0, length(string.digits)-1)
if get_new_idx == char_idx:
    get_new_idx  = 1
get_new = string.digits[get_new_idx]

Your best bet is to make a function for this and call it for each category (pass the character and category string as parameters). You can even move the check for the category into the function and return None if it's not in the category, but I'll leave that for you to try.

Hope that helps a little.

CodePudding user response:

I'd suggest not doing lots in operations where the RHS is a list. This takes O(n) time so all your if statements are very slow. I'd also note that by rejecting the same character as input you're biasing the result, which was part of the why the Enigma machine was broken.

I'd do something like:

from random import choice
from string import digits, ascii_lowercase, ascii_uppercase, punctuation

# generate a dictionary once that turns characters into the char classes
# that can be used to draw replacements from
CHAR_CLASSES = {
    c: charclass
    for charclass in (digits, ascii_lowercase, ascii_uppercase, punctuation)
    for c in charclass
}

def _randomizer(c):
    "get a replacement character drawn from the same class"
    charclass = CHAR_CLASSES.get(c)
    return choice(charclass) if charclass else c

def randomize(text):
    return ''.join(map(_randomizer, text))

print(randomize("Hello World 123"))

You could golf this code down if you want, but I've tried to make it somewhat verbose and readable.

If you really want to exclude characters from even appearing as themselves, you could replace the initialization of CHAR_CLASSES with:

CHAR_CLASSES = {
    c: charclass.replace(c, '')
    for charclass in (digits, ascii_lowercase, ascii_uppercase, punctuation)
    for c in charclass
}

but I'd suggest not doing that!

  • Related