### 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: EASY
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):
	ans = ''
	clean_text = ''.join([i.upper() for i in text if i.isalpha()])
	clean_text += 'Z' * (len(clean_text) % 2)
	if len(clean_text) == 0:
		return ''
	km = np.matrix([[ord(key[0]) - 97, ord(key[1]) - 97], [ord(key[2]) - 97, ord(key[3]) - 97]])
	tm = np.matrix([[ord(clean_text[i]) - 65, ord(clean_text[i + 1]) - 65] for i in range(0, len(clean_text), 2)]).T
	am = km * tm % 26
	for i in np.array(am.T):
		ans += chr(i[0] + 65)
		ans += chr(i[1] + 65)
	return ans


from string import ascii_letters
import numpy as np

def encrypt(text, key):
	code = lambda s: ord(s.lower()) - ord('a')
	letter = lambda n: chr(n + ord('A'))
	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)
	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()
	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))


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('[^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('[^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