Matrices-Practice-Tool / src / QuestionClasses / question.py
question.py
Raw
'''Question objects'''

from ast import literal_eval as l_e
import sys
import random
import os
from os import listdir
from os.path import isfile, join
import numpy as np
from QuestionClasses.question_file_writer import QuestionFileWriter

WARNING = '\033[93m'
BOUND = (-20, 20)


class Question:
    '''Class for holding question data'''
    q_type = 0
    matrices_dict = {}
    answer_matrix = 0
    operation = ''

    def __init__(self, data: list):
        '''Initialiser'''
        self.matrices_dict = {}
        self.set_constants(data)
        self.set_answer_data(data)
        self.set_question_data(data)
        q_writer = QuestionFileWriter(self.get_file_name(), self)
        q_writer.write_to()

    @classmethod
    def from_file(cls, file_name: str):
        '''Constructor to initialise from file'''
        try:
            with open(file_name, 'r') as f:
                data = f.readline().split('$')
                return cls(data)
        except FileNotFoundError as e:
            print(f'{WARNING}{e}')
            sys.exit(1)

    @classmethod
    def from_user_input(cls, data: list) -> 'Question':
        '''Constructor to initialise from user inputted values'''
        return cls(data)

    @classmethod
    def random_any(cls) -> 'Question':
        '''Constructor to initialise random question of any type'''
        data = random.choice([cls.random_add, cls.random_sub,
                              cls.random_mult, cls.random_det,
                              cls.random_inv])
        return cls(data)

    @classmethod
    def random_add(cls) -> 'Question':
        '''Constructor to initialise random addition question'''
        data = []
        dim = tuple(cls.random_list((2, 10), 2))
        size = dim[0] * dim[1]
        matrix1 = cls.create_matrix_string('A', size, dim)
        matrix2 = cls.create_matrix_string('B', size, dim)
        answer_matrix = np.add(cls.list_to_np(matrix1, dim),
                               cls.list_to_np(matrix2, dim))
        answer_values = cls.np_to_list(answer_matrix)
        answer = [str(dim), str(answer_values)]
        data.extend((1,
                     '%'.join(matrix1),
                     '%'.join(matrix2),
                     '+',
                     '%'.join(answer)))
        return cls(data)

    @classmethod
    def random_sub(cls) -> 'Question':
        '''Constructor to initialise random subtraction question'''
        data = []
        dim = tuple(cls.random_list((2, 10), 2))
        size = dim[0] * dim[1]
        matrix1 = cls.create_matrix_string('A', size, dim)
        matrix2 = cls.create_matrix_string('B', size, dim)
        answer_matrix = np.subtract(cls.list_to_np(matrix1, dim),
                                    cls.list_to_np(matrix2, dim))
        answer_values = cls.np_to_list(answer_matrix)
        answer = [str(dim), str(answer_values)]
        data.extend((2,
                     '%'.join(matrix1),
                     '%'.join(matrix2),
                     '-',
                     '%'.join(answer)))
        return cls(data)

    @classmethod
    def random_mult(cls) -> 'Question':
        '''Constructor to initialise random multiplication question'''
        data = []
        dim1 = (random.randint(2, 5), random.randint(2, 5))
        dim2 = (dim1[1], random.randint(2, 5))
        matrix1 = cls.create_matrix_string('A', dim1[0] * dim1[1], dim1)
        matrix2 = cls.create_matrix_string('B', dim2[0] * dim2[1], dim2)
        answer_matrix = np.dot(cls.list_to_np(matrix1, dim1),
                               cls.list_to_np(matrix2, dim2))
        answer_values = cls.np_to_list(answer_matrix)
        answer = [str((dim1[0], dim2[1])), str(answer_values)]
        data.extend((3,
                     '%'.join(matrix1),
                     '%'.join(matrix2),
                     '*',
                     '%'.join(answer)))
        return cls(data)

    @classmethod
    def random_det(cls) -> 'Question':
        '''Constructor to initialise random determinant question'''
        data = []
        i = random.randint(2, 3)
        dim = tuple(cls.random_list((i, i), 2))
        size = dim[0] * dim[1]

        matrix1 = cls.create_matrix_string('A', size, dim)
        answer_matrix = np.linalg.det(cls.list_to_np(matrix1, dim))
        answer_value = cls.np_to_list(answer_matrix)

        answer = ['(1, 1)', answer_value]
        data.extend((4,
                     '%'.join(matrix1),
                     'det',
                     '%'.join(answer)))
        return cls(data)

    @classmethod
    def random_inv(cls) -> 'Question':
        '''Constructor to initialise random inverse question'''
        data = []
        i = random.randint(2, 3)
        dim = tuple(cls.random_list((i, i), 2))

        size = dim[0] * dim[1]
        matrix1 = cls.create_matrix_string('A', size, dim)
        answer_matrix = np.linalg.inv(cls.list_to_np(matrix1, dim))
        answer_values = cls.np_to_list(answer_matrix)
        answer = [str((dim[1], dim[0])), str(answer_values)]
        data.extend((5,
                     '%'.join(matrix1),
                     'inv',
                     '%'.join(answer)))
        return cls(data)

    @staticmethod
    def np_to_list(matrix: np.array) -> list:
        '''Convert numpy array to list'''
        return str(matrix.flatten().tolist())

    @staticmethod
    def list_to_np(m: list, dim: tuple) -> np.array:
        '''Conver list to numpy array'''
        return np.array(l_e(m[2])).reshape(dim)

    @staticmethod
    def random_list(b: tuple, size: int) -> list:
        '''Create a list of random integers given a size'''
        return [random.randint(b[0], b[1]) for _ in range(size)]

    @staticmethod
    def create_matrix_string(name: str, size: int, dim: tuple) -> list:
        '''Create a string of matrix data'''
        matrix_values = Question.random_list(BOUND, size)
        return [name, str(dim), str(matrix_values)]

    def set_question_data(self, data: list):
        '''Set question data'''
        q_data = []
        for i in range(1, len(data) - 2):
            q_data.append(data[i].split('%'))
            self.matrices_dict[q_data[i - 1][0]] = (np.array(l_e(q_data[i - 1][2]))
                                                    .reshape(l_e(q_data[i - 1][1])))

    def set_answer_data(self, data: list):
        '''Set answer data'''
        a_data = data[-1].split('%')
        self.answer_matrix = np.array(l_e(a_data[1])).reshape(l_e(a_data[0]))

    def set_constants(self, data: list):
        '''Set question constants'''
        self.q_type, self.operation = data[0], data[-2]

    def show_answer(self):
        '''Printing answer'''
        print(f'=\n{self.answer_matrix}\n')

    def show_matrices(self):
        '''Printing matrices'''
        for i, (key, matrix) in enumerate(self.matrices_dict.items()):
            print(f'{key}:\n{matrix}\n')
            if i != len(self.matrices_dict) - 1:
                print(f'{self.operation}\n')
        self.show_answer()

    def get_file_name(self) -> str:
        '''Get file name for new question'''
        path = self.get_path()
        file_numbers = self.get_file_numbers(path)
        next_file_number = max(list(map(int, file_numbers))) + \
            1 if len(file_numbers) > 0 else 1
        self.num = next_file_number
        return f'{path}/q{next_file_number}.txt'

    @staticmethod
    def get_path() -> str:
        '''Get path to find existing questions'''
        file_dir = os.path.dirname(os.path.realpath('__file__'))
        return os.path.join(file_dir, 'questions')

    @staticmethod
    def get_file_numbers(path: str) -> list:
        '''Get the existing question file numbers'''
        file_names = [f for f in listdir(path) if isfile(join(path, f))]
        file_numbers = []
        for name in file_names:
            file_numbers.append(''.join(c for c in name if c.isdigit()))
        return file_numbers


if __name__ == "__main__":
    Question.random_any()