The trifid cipher uses a table to fractionate each plaintext letter into a trigram, mixes the constituents of the trigrams, and then applies the table in reverse to turn these mixed trigrams into ciphertext letters. https:en.wikipedia.orgwikiTrifidcipher fmt: off fmt: off Arrange the triagram value of each letter of 'messagepart' vertically and join them horizontally.  encryptpart'ASK', TESTCHARACTERTONUMBER '132111112' Convert each letter of the input string into their respective trigram values, join them and split them into three equal groups of strings which are returned.  decryptpart'ABCDE', TESTCHARACTERTONUMBER '11111', '21131', '21122' A helper function that generates the triagrams and assigns each letter of the alphabet to its corresponding triagram and stores this in a dictionary charactertonumber and numbertocharacter after confirming if the alphabet's length is 27.  test  prepare'I aM a BOy','abCdeFghijkLmnopqrStuVwxYZ'  expected  'IAMABOY','ABCDEFGHIJKLMNOPQRSTUVWXYZ', ... TESTCHARACTERTONUMBER, TESTNUMBERTOCHARACTER  test  expected True Testing with incomplete alphabet  prepare'I aM a BOy','abCdeFghijkLmnopqrStuVw' Traceback most recent call last: ... KeyError: 'Length of alphabet has to be 27.' Testing with extra long alphabets  prepare'I aM a BOy','abCdeFghijkLmnopqrStuVwxyzzwwtyyujjgfd' Traceback most recent call last: ... KeyError: 'Length of alphabet has to be 27.' Testing with punctuations that are not in the given alphabet  prepare'am i a boy?','abCdeFghijkLmnopqrStuVwxYZ' Traceback most recent call last: ... ValueError: Each message character has to be included in alphabet! Testing with numbers  prepare500,'abCdeFghijkLmnopqrStuVwxYZ' Traceback most recent call last: ... AttributeError: 'int' object has no attribute 'replace' Validate message and alphabet, set to upper and remove spaces Check length and characters Generate dictionares encryptmessage  Encrypts a message using the trifidcipher. Any punctuatuions that would be used should be added to the alphabet. PARAMETERS     message: The message you want to encrypt.    alphabet optional: The characters to be used for the cipher .    period optional: The number of characters you want in a group whilst encrypting.  encryptmessage'I am a boy' 'BCDGBQY'  encryptmessage' ' ''  encryptmessage'   aide toi le c  iel      ta id  era    ', ... 'FELIXMARDSTBCGHJKNOPQUVWYZ',5 'FMJFVOISSUFTFPUFEQQC' decryptmessage  Decrypts a trifidcipher encrypted message . PARAMETERS     message: The message you want to decrypt .    alphabet optional: The characters used for the cipher.    period optional: The number of characters used in grouping when it was encrypted.  decryptmessage'BCDGBQY' 'IAMABOY' Decrypting with your own alphabet and period  decryptmessage'FMJFVOISSUFTFPUFEQQC','FELIXMARDSTBCGHJKNOPQUVWYZ',5 'AIDETOILECIELTAIDERA'
from __future__ import annotations

# fmt: off
TEST_CHARACTER_TO_NUMBER = {
    "A": "111", "B": "112", "C": "113", "D": "121", "E": "122", "F": "123", "G": "131",
    "H": "132", "I": "133", "J": "211", "K": "212", "L": "213", "M": "221", "N": "222",
    "O": "223", "P": "231", "Q": "232", "R": "233", "S": "311", "T": "312", "U": "313",
    "V": "321", "W": "322", "X": "323", "Y": "331", "Z": "332", "+": "333",
}
# fmt: off

TEST_NUMBER_TO_CHARACTER = {val: key for key, val in TEST_CHARACTER_TO_NUMBER.items()}


def __encrypt_part(message_part: str, character_to_number: dict[str, str]) -> str:
    """
    Arrange the triagram value of each letter of 'message_part' vertically and join
    them horizontally.

    >>> __encrypt_part('ASK', TEST_CHARACTER_TO_NUMBER)
    '132111112'
    """
    one, two, three = "", "", ""
    for each in (character_to_number[character] for character in message_part):
        one += each[0]
        two += each[1]
        three += each[2]

    return one + two + three


def __decrypt_part(
    message_part: str, character_to_number: dict[str, str]
) -> tuple[str, str, str]:
    """
    Convert each letter of the input string into their respective trigram values, join
    them and split them into three equal groups of strings which are returned.

    >>> __decrypt_part('ABCDE', TEST_CHARACTER_TO_NUMBER)
    ('11111', '21131', '21122')
    """
    this_part = "".join(character_to_number[character] for character in message_part)
    result = []
    tmp = ""
    for digit in this_part:
        tmp += digit
        if len(tmp) == len(message_part):
            result.append(tmp)
            tmp = ""

    return result[0], result[1], result[2]


def __prepare(
    message: str, alphabet: str
) -> tuple[str, str, dict[str, str], dict[str, str]]:
    """
    A helper function that generates the triagrams and assigns each letter of the
    alphabet to its corresponding triagram and stores this in a dictionary
    ("character_to_number" and "number_to_character") after confirming if the
    alphabet's length is 27.

    >>> test = __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxYZ+')
    >>> expected = ('IAMABOY','ABCDEFGHIJKLMNOPQRSTUVWXYZ+',
    ... TEST_CHARACTER_TO_NUMBER, TEST_NUMBER_TO_CHARACTER)
    >>> test == expected
    True

    Testing with incomplete alphabet
    >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVw')
    Traceback (most recent call last):
        ...
    KeyError: 'Length of alphabet has to be 27.'

    Testing with extra long alphabets
    >>> __prepare('I aM a BOy','abCdeFghijkLmnopqrStuVwxyzzwwtyyujjgfd')
    Traceback (most recent call last):
        ...
    KeyError: 'Length of alphabet has to be 27.'

    Testing with punctuations that are not in the given alphabet
    >>> __prepare('am i a boy?','abCdeFghijkLmnopqrStuVwxYZ+')
    Traceback (most recent call last):
        ...
    ValueError: Each message character has to be included in alphabet!

    Testing with numbers
    >>> __prepare(500,'abCdeFghijkLmnopqrStuVwxYZ+')
    Traceback (most recent call last):
        ...
    AttributeError: 'int' object has no attribute 'replace'
    """
    # Validate message and alphabet, set to upper and remove spaces
    alphabet = alphabet.replace(" ", "").upper()
    message = message.replace(" ", "").upper()

    # Check length and characters
    if len(alphabet) != 27:
        raise KeyError("Length of alphabet has to be 27.")
    if any(char not in alphabet for char in message):
        raise ValueError("Each message character has to be included in alphabet!")

    # Generate dictionares
    character_to_number = dict(zip(alphabet, TEST_CHARACTER_TO_NUMBER.values()))
    number_to_character = {
        number: letter for letter, number in character_to_number.items()
    }

    return message, alphabet, character_to_number, number_to_character


def encrypt_message(
    message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5
) -> str:
    """
    encrypt_message
    ===============

    Encrypts a message using the trifid_cipher. Any punctuatuions that
    would be used should be added to the alphabet.

    PARAMETERS
    ----------

    *   message: The message you want to encrypt.
    *   alphabet (optional): The characters to be used for the cipher .
    *   period (optional): The number of characters you want in a group whilst
        encrypting.

    >>> encrypt_message('I am a boy')
    'BCDGBQY'

    >>> encrypt_message(' ')
    ''

    >>> encrypt_message('   aide toi le c  iel      ta id  era    ',
    ... 'FELIXMARDSTBCGHJKNOPQUVWYZ+',5)
    'FMJFVOISSUFTFPUFEQQC'

    """
    message, alphabet, character_to_number, number_to_character = __prepare(
        message, alphabet
    )

    encrypted_numeric = ""
    for i in range(0, len(message) + 1, period):
        encrypted_numeric += __encrypt_part(
            message[i : i + period], character_to_number
        )

    encrypted = ""
    for i in range(0, len(encrypted_numeric), 3):
        encrypted += number_to_character[encrypted_numeric[i : i + 3]]
    return encrypted


def decrypt_message(
    message: str, alphabet: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ.", period: int = 5
) -> str:
    """
    decrypt_message
    ===============

    Decrypts a trifid_cipher encrypted message .

    PARAMETERS
    ----------

    *   message: The message you want to decrypt .
    *   alphabet (optional): The characters used for the cipher.
    *   period (optional): The number of characters used in grouping when it
        was encrypted.

    >>> decrypt_message('BCDGBQY')
    'IAMABOY'

    Decrypting with your own alphabet and period
    >>> decrypt_message('FMJFVOISSUFTFPUFEQQC','FELIXMARDSTBCGHJKNOPQUVWYZ+',5)
    'AIDETOILECIELTAIDERA'
    """
    message, alphabet, character_to_number, number_to_character = __prepare(
        message, alphabet
    )

    decrypted_numeric = []
    for i in range(0, len(message), period):
        a, b, c = __decrypt_part(message[i : i + period], character_to_number)

        for j in range(len(a)):
            decrypted_numeric.append(a[j] + b[j] + c[j])

    return "".join(number_to_character[each] for each in decrypted_numeric)


if __name__ == "__main__":
    import doctest

    doctest.testmod()
    msg = "DEFEND THE EAST WALL OF THE CASTLE."
    encrypted = encrypt_message(msg, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ")
    decrypted = decrypt_message(encrypted, "EPSDUCVWYM.ZLKXNBTFGORIJHAQ")
    print(f"Encrypted: {encrypted}\nDecrypted: {decrypted}")
