Caesar Salad: Weekly Challenge 358 part 2

Task 2: Encrypted String:

You are given a string $str and an integer $int.

 
Write a script to encrypt the string using the algorithm — for each character $char in $str, replace $char with the $int th character after $char in the alphabet, wrapping if needed and return the encrypted string.

"In the alphabet" tells me only letters should change, not any other character, and "In the alphabet" tells me only A-Z (in either case). YMMV: This is certainly a more interesting problem if accented characters were included, but code that works with only the base 52 letters will also work with accented characters in Unicode Normalization Form D[1] (or KD if desired), so I'm going to say we can leave that up to the caller.

In Python:

def encrypted_string(string: str, shift: int) -> str:
    return string.translate({a+i: a+(shift+i)%26 for i in range(26) for a in (ord('a'),ord('A'))})

I often have a conceptual problem with string.translate: I expect it to do everything that Perl's transliteration operator does, and when it doesn't, I find another way. Here, it just works, and without even needing string.maketrans (since we are doing math and want the codepoints anyway).

This represents one of the two simplest ways to approach this problem: make a mapping table and apply it to the string. The other way is to just loop over the string and decide how to handle each character, an approach that is far more natural in Go.

In Go:

func EncryptedString(str string, shift int) string {
    shiftRune := rune(shift % 26)
    runes := []rune(str)
    encryptedRunes := make([]rune, len(runes))
    for i, r := range runes {
        if r >= 'a' && r <= 'z' {
            r = 'a' + (r - 'a' + shiftRune) % 26
        } else if r >= 'A' || r <= 'Z' {
            r = 'A' + (r - 'A' + shiftRune) % 26
        }
        encryptedRunes[i] = r
    }
    return string(encryptedRunes)
}

Really not much to see here. Modding the input by 26 outside the loop for efficiency, but also to cast to a rune to avoid type errors later — the rune type (the type of a character literal like 'A') is interoperable with int32 but not int. The function argument could be int32, of course, but I like sticking to more basic types in interfaces.

In Perl:

sub encrypted_string($str, $int) {
    my %encrypt = map((chr($_+ord 'A'), chr(($_+$int) % 26 + ord 'A'), chr($_+ord 'a'), chr(($_+$int) % 26 + ord 'a')), 0..25);
    return $str =~ s/([a-z])/$encrypt{$1}/igr;
}

Transliteration is more efficient than global substitution in Perl, but suffers the defect of requiring eval to have dynamic replacement lists. For simplicity, I'm just doing the global substitution.

While I find Perl's map more readable than Python comprehensions, nested maps are sadly not; here I just repeat code rather than introduce a nested map to loop over ord 'a' and ord 'A'. Maybe some syntax like map:topic($a) ... someday?

full script, Python
full script, Go
full script, Perl

Comments greatly appreciated. See you next week.


  1. https://unicode.org/reports/tr15/#Norm_Forms ↩︎





Read more