### Background
In classical cryptography, the Hill cipher is a polygraphic substitution cipher based on linear algebra. It was invented by Lester S. Hill in 1929.



### Task


This cipher involves a text key which has to be turned into a matrix and text which needs to be encoded. The text key can be of any perfect square length but for the sake of this kata we will focus on keys of length 4 forming a 2x2 matrix.

To encrypt a message using the hill cipher, first of all you need to convert the text key into a key matrix. To do that you will convert the key row wise into a 2x2 matrix. Then you will substitute the letters with their respective positions on the alphabet: A=0, B=1,C=2 and so on till Z=25. So for example if we get the key text as ```cats```, the key matrix will be:
    
    [[ 2  0]
     [19 18]]
     
Now the next step is to break the text into pairs of two and convert those pairs into 2x1 matrices. If your text has an odd number of letters then just add a Z next to your last letter. Now again convert those letters into their respective position in the alphabet as above. So for example the text ```Hi``` would be converted into:

    [[7]
     [8]]
     
Now we need to [multiply](https://www.mathsisfun.com/algebra/matrix-multiplying.html) the key matrix by the text matrix to get our encrypted matrix and then find out the encrypted matrix [modulo](https://en.wikipedia.org/wiki/Modulo_operation) 26:

    [[ 2  0]  *  [[7]  =  [[14]   =  [[14]  mod 26
     [19 18]]     [8]]     [277]]     [17]]
     
For the final step we just find out the letters at the alphabet position of 14 and 17 which are ```O``` and ```R```. So ```OR``` is our encrypted message for the message ```Hi```


In this kata you will be given a function named ```encrypt``` with the parameters ```text``` and ```key``` and you have to return the encrypted message in all uppercase letters

``` python
encrypt('','azyb') → ''
encrypt('Hi','cats') → 'OR'
encrypt('This is a good day','bbaa') → 'AAAAAAGACAGAYA'
```

Note:
- The text to encrypt will contain characters other than the alphabets, its your job to clean them before converting text to matrices. Spaces also need to be removed
- The text may contain both uppercase and lowercase alphabets. Its your job to standardize them, the encrypted text however should be returned in uppercase letters.
- The key will always contain 4 lowercase alphabet.

Difficulty: introductory

Starter code:

def encrypt(text,key):
	

Solutions:
import numpy as np
from itertools import zip_longest
from string import ascii_lowercase as lower, ascii_uppercase as upper

D = {c:i%26 for i,c in enumerate(lower+upper)}

def encrypt(text, key):
    result = []
    text = ''.join(filter(str.isalpha, text))
    key = np.array(([D[key[0]], D[key[1]]], [D[key[2]], D[key[3]]]))
    for c1,c2 in zip_longest(text[::2], text[1::2], fillvalue='Z'):
        x,y = key @ ([D[c1]], [D[c2]])
        result.append(upper[x[0]%26] + upper[y[0]%26])
    return ''.join(result)

import numpy as np

def encrypt(text,key):
    # Declare string variable to store the answer
    ans=''
    # Covert all the alphabets to upper case and clean everything else
    clean_text=''.join([i.upper() for i in text if i.isalpha()])
    # Add Z to the cleaned text if the length of it is odd
    clean_text+='Z'*(len(clean_text)%2)
    # Return empty string if the cleaned text is empty
    if len(clean_text)==0:
        return ''
    # Find the key matrix
    km=np.matrix([[ord(key[0])-97,ord(key[1])-97],[ord(key[2])-97,ord(key[3])-97]])
    # Create a text matrix
    tm=np.matrix([[ord(clean_text[i])-65,ord(clean_text[i+1])-65] for i in range(0,len(clean_text),2)]).T
    # Multiply the key matrix by the text matrix and apply modulo 26
    am=(km*tm)%26
    # Convert the numbers back to letters and append them to the ans variable
    for i in np.array(am.T): 
        ans+=chr(i[0]+65)
        ans+=chr(i[1]+65)
    # Return the answer variable    
    return ans

from string import ascii_letters
import numpy as np

def encrypt(text, key):
    # Character and interger conversions
    code = lambda s: ord(s.lower()) - ord('a')
    letter = lambda n: chr(n + ord('A'))

    # Build key
    size = int(len(key) ** 0.5)
    if size ** 2 != len(key):
        raise ValueError('Key must be of length n^2')    
    key_matrix = np.array([code(c) for c in key]).reshape(size, size)
    
    # Filter out non letters and pad if necessary
    text = ''.join(c for c in text if c in ascii_letters)
    if len(text) % size != 0:
        text += 'z' * (size - (len(text) % size))
    text_matrix = np.array([code(c) for c in text]).reshape(len(text) // size, size).transpose()
    
    # Perform encoding
    encoded = (key_matrix @ text_matrix) % 26
    return ''.join(letter(n) for n in encoded.transpose().flatten())


def encrypt(text,key):
    final_list=[]
    if text=="":
        return ""
    alpha_dict={}
    num_lst_1=[]
    alphabet="abcdefghijklmnopqrstuvwxyz"
    for i in range(0,26):
        alpha_dict[alphabet[i]]=i
    for letter in text.lower():
        if letter in alphabet:
            num_lst_1.append(alpha_dict[letter])
    if len(num_lst_1)%2==1:
        num_lst_1.append(25)
    num_lst_2=[]
    for char in key.lower():
        num_lst_2.append(alpha_dict[char])
    sum_row_1=0
    sum_row_2=0
    for x in range(0,len(num_lst_1),2):
        sum_row_1=num_lst_2[0]*num_lst_1[x]+num_lst_2[1]*num_lst_1[x+1]
        sum_row_2=num_lst_2[2]*num_lst_1[x]+num_lst_2[3]*num_lst_1[x+1]
        final_list.append(sum_row_1%26)
        final_list.append(sum_row_2%26)
    enpt=""
    for num in final_list:
        enpt+=alphabet[num]
    return enpt.upper()


import re

def encrypt(text, key):
    a, b, c, d = (ord(t) - 65 for t in key.upper())
    r = ''
    for x, y in zip(*[iter(re.findall('[A-Z]', text.upper()) + ['Z'])] * 2):
        x, y = (ord(t) - 65 for t in (x, y))
        x, y = (a * x + b * y) % 26, (c * x + d * y) % 26
        r += chr(x + 65) + chr(y + 65)
    return r


def encrypt(text,key):
    r =''
    a = 'abcdefghijklmnopqrstuvwxyz'
    km = [[a.index(key[0]),a.index(key[1])],[a.index(key[2]),a.index(key[3])]]
    text = ''.join(x for x in text.lower() if x in a)
    if len(text)%2: text += 'z'
    for i in range(0,len(text),2):
        tm = [a.index(text.lower()[i]),a.index(text.lower()[i+1])]
        r += a.upper()[(km[0][0]*tm[0]+km[0][1]*tm[1])%26]+a.upper()[(km[1][0]*tm[0]+km[1][1]*tm[1])%26]
    return r

def encrypt(text,key):
    k = [ord(i) - 97 for i in key]
    text = ''.join(c for c in text if c.isalpha())
    text = text.upper() + "Z"*(len(text) % 2)
    nums = [ord(c) - 65 for c in text]
    Out = ''
    for i, j in zip(nums[::2],nums[1::2]):
        Out += chr((k[0]*i + k[1]*j)%26 + 65) + chr((k[2]*i + k[3]*j)%26 + 65)
    return Out

from string import ascii_uppercase as letters

def encrypt(text, key):
    key = [[letters.index(c) for c in key.upper()[i:i+2]] for i in (0,2)]
    text = [letters.index(c) for c in text.upper() if c.isalpha()]
    if len(text) % 2 != 0: text.append(letters.index('Z'))
    out = []
    for i in range(0, len(text), 2):
        for j in range(2):
            out.append((key[j][0]*text[i] + key[j][1]*text[i+1]) % 26)
    return ''.join(letters[n] for n in out)

"""by Zakhar M."""
import re
import numpy as np
from math import gcd


ALPHABET = {chr(letters): letters - 65 for letters in range(65, 91)}
REVERSE_ALPHABET = {number_symbol: symbol for symbol, number_symbol in list(ALPHABET.items())}


def checking_key_for_validity(matrix_key, length_alphabet):
    determinant = np.linalg.det(matrix_key)
    nod_determinant_and_length_alphabet = gcd(int(round(determinant)), length_alphabet)

    if nod_determinant_and_length_alphabet == 1 and nod_determinant_and_length_alphabet != 0:
        return True
    return False


def get_matrix_key(key, length_block=2):
    tmp_list = list()
    matrix_key = list()
    length_key = len(key)

    if length_key % 2 != 0:
        return ''

    for symbol in key:
        number_symbol_in_alphabet = ALPHABET[symbol]
        tmp_list.append(number_symbol_in_alphabet)
        length_tmp_list = len(tmp_list)

        if length_tmp_list != length_block:
            continue

        matrix_key.append(tmp_list)
        tmp_list = list()

    return matrix_key


def get_matrix_open_text(open_text, length_blocks=2):
    tmp_list = list()
    matrix_open_text = list()
    pattern_only_letters = re.compile(r'[^a-zA-Z]*')
    prepared_open_text = pattern_only_letters.sub('', open_text).upper()
    length_prepared_open_text = len(prepared_open_text)

    if length_prepared_open_text % 2 != 0:
        prepared_open_text += 'Z'

    for symbol in prepared_open_text:
        number_symbol_in_alphabet = ALPHABET[symbol]
        tmp_list.append(number_symbol_in_alphabet)
        length_tmp_list = len(tmp_list)

        if length_tmp_list != length_blocks:
            continue

        matrix_open_text.append(tmp_list)
        tmp_list = list()
        
    return matrix_open_text


def smart_mul_matrix(matrix_1, matrix_2):
    result_matrix = list()
    
    for i in range(len(matrix_1)):
        tmp_sum = 0
        
        for j in range(len(matrix_2)):
            tmp_sum += (matrix_1[i][j] * matrix_2[j])
        result_matrix.append(tmp_sum % 26)
        
    return result_matrix

def encrypt(open_text, key):
    cipher_text = ''
    length_alphabet = len(ALPHABET)
    matrix_cipher_text = list()
    
    pattern_only_letters = re.compile(r'[^a-zA-Z]*')
    prepared_key_text = pattern_only_letters.sub('', key).upper()
    matrix_key = get_matrix_key(prepared_key_text)
    
    matrix_open_text = get_matrix_open_text(open_text)
    
    for current_list_open_text in matrix_open_text:
        matrix_multiplication = smart_mul_matrix(matrix_key, current_list_open_text)
        matrix_cipher_text.append(matrix_multiplication)
        
    for list_cipher_text in matrix_cipher_text:
        for number_cipher_text in list_cipher_text:
            cipher_text += REVERSE_ALPHABET[number_cipher_text]
    return cipher_text



def encrypt(text,key):
    import numpy as np
    import string

    mapowanie_dig_str=dict(zip([x for x in range(0,26)],list(string.ascii_uppercase)))
    mapowanie_str_dig=dict(zip(list(string.ascii_uppercase),[x for x in range(0,26)]))
    
    l_key=[]
    for s in key.upper():
        l_key=l_key+[mapowanie_str_dig[s]]


    key_array_dig=np.array([l_key[0:2],l_key[2:]])
    correct_text=''
    for s in text.upper():
        if s in string.ascii_uppercase:
            correct_text=correct_text+s
            
    if len(correct_text)%2!=0:
       correct_text=correct_text+'Z' 
    
    output=''
    l_core=list(correct_text)   
    while l_core!=[]:
        pice_text_array=np.array([[mapowanie_str_dig[l_core[0]]],[mapowanie_str_dig[l_core[1]]]])
        l_core.pop(0)
        l_core.pop(0)
        P=(key_array_dig.dot(pice_text_array))%26
        output=output+mapowanie_dig_str[P[0][0]]+mapowanie_dig_str[P[1][0]]       
        
    return output