Задача №5

Дано два класи MultChecker (клас для перевірки таблиці множення - рандомно генеруються числа, які треба перемножати), AddChecker (клас для перевірки додавання - рандомно генеруються числа у заданому діапазоні, які треба додавати). Обидва класи надсилають результати тестування об'єкту класу History, який зберігає історію тестування у масиві у вигляді об'єктів.

Приклад:

testsList = [
  {
    firstNum: 1,
    secondNum: 5,
    operation: '*',
    userAnswer: 7,
    correctAnswer: 5,
  },
  {
    firstNum: 3,
    secondNum: 4,
    operation: '+',
    userAnswer: 7,
    correctAnswer: 7,
  },
]

Можна створити окремий клас TestData, який описує один такий тест і у якому будуть ці поля.

Розробити клас TestManager, який використовуючи ці класи за допомогою таймера періодично генерує якісь N задач (рандомно вибираємо, що опитувати: додавання чи множення) і проводить опитування. Результати тестування додаються в об’єкт History. Зробити так, щоб об'єкт такого класу можна було створити тільки один. Коли зроблено ці N задач вивести усю історію на екран.


Рішення:

'use strict'

/**
 * @typedef {'*' | '/' | '**' | '%' | '+' | '-'} operator
 */

/**
 * @param {number} min
 * @param {number} max
 */
function getRandomNum(min = 1, max = 9) {
  return min + Math.floor(Math.random() * (max - min + 1))
}

/** Provides math test objects. */
class MathQuestion {
  /** @type {Object<string, (a: number, b: number) => number>} */
  static operations = {
    '+': (a, b) => a + b,
    '*': (a, b) => a * b,
    '%': (a, b) => a % b,
    '-': (a, b) => a - b,
    '/': (a, b) => a / b,
    '**': (a, b) => a ** b,
  }

  /**
   * @param {operator} operator
   */
  constructor(operator) {
    this.firstNum = getRandomNum()
    this.secondNum = getRandomNum()
    this.operation = operator
    this.correctAnswer = MathQuestion.operations[operator](
      this.firstNum,
      this.secondNum,
    )
  }

  toString() {
    return `${this.firstNum} ${this.operation} ${this.secondNum} = ?`
  }
}

/** Handles history */
class HistoryManager {
  /** @type {any[]} */
  #history = []

  get entries() {
    return this.#history
  }

  clear() {
    this.#history = []
  }

  /** @param {any} value */
  save(value) {
    this.#history.push(value)
  }

  /** @param {any} value */
  delete(value) {
    const indexOfValue = this.#history.indexOf(value)

    if (indexOfValue !== -1) this.#history.splice(indexOfValue, 1)
  }

  toString() {
    return this.entries.map((entry) => String(entry)).join('\n')
  }
}

/** Creates an instance of a mathematical operation task. */
class TestData {
  /**
   * @param {Object} params - The parameters for the task.
   * @param {number} params.firstNum - The first number in the operation.
   * @param {number} params.secondNum - The second number in the operation.
   * @param {string} params.operation - The mathematical operation (e.g., "+", "-", "*", "/").
   * @param {string} params.userAnswer - The answer provided by the user.
   * @param {number} params.correctAnswer - The correct answer to the operation.
   */
  constructor({firstNum, secondNum, operation, userAnswer, correctAnswer}) {
    this.firstNum = firstNum
    this.secondNum = secondNum
    this.operation = operation
    this.userAnswer = userAnswer
    this.correctAnswer = correctAnswer
  }

  /** Use Number instead of parseInt to enforce strict answers */
  isCorrect() {
    return this.correctAnswer === Number(this.userAnswer)
  }

  toString() {
    return `${this.firstNum} ${this.operation} ${this.secondNum} = ${this.correctAnswer}. ${this.isCorrect() ? '' : ''} Ваша відповідь: ${this.userAnswer}.`
  }
}

/** Handles mathematical question and saves them to memory. Only one instance may be created due to a singleton pattern. */
class TestManager {
  /** @type {TestManager} */
  static #instance

  static get instance() {
    return this.#instance
  }

  /**
   * @param {string} selector - An CSS selector to identify where to render the statistics.
   * @param {operator[]} operators - An array of JS operators to use in tests.
   */
  constructor(selector, operators = ['*', '%', '+', '-', '/', '**']) {
    if (TestManager.instance) return TestManager.instance

    /** @type {HistoryManager} */
    this.history = new HistoryManager()
    this.outputElement = document.querySelector(selector)
    this.operators = operators
    TestManager.#instance = this
  }

  /** @param {operator} operation */
  askQuestion(operation) {
    const question = new MathQuestion(operation)
    const userAnswer = prompt(String(question)) ?? '-'

    this.history.save(new TestData({...question, userAnswer}))
  }

  getRandomOperation() {
    const index = getRandomNum(0, this.operators.length - 1)

    return this.operators[index]
  }

  /** @param {number} times */
  start(times, intervalSeconds = 1) {
    this.askQuestion(this.getRandomOperation())

    if (times <= 1) {
      this.renderStatsMarkup()

      return
    }

    setTimeout(() => {
      this.start(times - 1, intervalSeconds)
    }, intervalSeconds * 1000)
  }

  getStatsMarkup() {
    return `
      <div class="u-p-200 u-border u-rounded-lg u-flow-200">
        <h3>Приклади 📊</h3>
        <ol>
          ${this.history.entries.map((entry) => `<li>${String(entry)}</li>`).join('')}
        </ol>
      </div>
    `
  }

  renderStatsMarkup() {
    if (!this.outputElement)
      throw new Error("Can't find an HTML container to insert statistics.")

    this.outputElement.innerHTML = this.getStatsMarkup()
  }
}

// ---

if (confirm('Почати тестування?')) {
  try {
    const mathColloquium = new TestManager('#output', ['*', '%', '+', '-'])

    mathColloquium.start(4)
  } catch (error) {
    alert(error)
  }
}