Пишем калькулятор на JavaScript

  • Tutorial


Доброго времени суток, друзья!

В этой статье мы с вами, как следует из названия, напишем простой калькулятор на JavaScript.

Желание написать калькулятор возникло у меня после просмотра одного туториала, посвященного созданию «simple calculator», который оказался далеко не симпл и толком ничего не умел делать.

Наш калькулятор будет true simple (42 строки кода, включая пробелы между блоками), но при этом полнофункциональным и масштабируемым.

Для расчетов будет использоваться эта замечательная библиотека (Math.js).

Без дальнейших предисловий, приступаем к делу.

Наша разметка выглядит так:

<!-- head -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/6.6.4/math.js"></script>

<!-- body -->
<div class="calculator">
    <output></output>
</div>

Здесь мы подключаем библиотеку, создаем контейнер для калькулятора и поле для вводимых символов и результата.

Подключаем стили:
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  height: 100vh;
  background: radial-gradient(circle, skyblue, steelblue);
  display: flex;
  justify-content: center;
  align-items: center;
}

.calculator {
  width: 320px;
  height: 480px;
  background: #eee;
  border-radius: 5px;
  box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.2), -2px -2px 3px rgba(0, 0, 0, 0.2);
}

output {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 300px;
  height: 40px;
  background: #fff;
  margin: 10px auto;
  border-radius: 5px;
  font-size: 1.4em;
  font-weight: bold;
  box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.3),
    inset -1px -1px 1px rgba(0, 0, 0, 0.3);
}

.keyboard {
  height: 440px;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-evenly;
  align-items: flex-start;
}

button {
  margin: 0.5em 1em;
  width: 40px;
  height: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1em;
  font-weight: bold;
}


Вот что мы имеем на данный момент:



Кнопки будут генерироваться программно.

Переходим к скрипту.

Определяем поле для вывода результата и создаем контейнер для клавиатуры:

const output = document.querySelector('output')

const div = document.createElement('div')
div.classList.add('keyboard')
document.querySelector('.calculator').appendChild(div)

Наша строка с символами выглядит так:

'C CE % / 7 8 9 * 4 5 6 - 1 2 3 + 0 ( ) ='

Преобразуем данную строку в массив и создаем кнопки:

// разделителем служит пустая строка
// можно было бы обойтись без пробелов, если бы у нас не было "CE"
'C CE % / 7 8 9 * 4 5 6 - 1 2 3 + 0 ( ) ='.split(' ')
    // пробегаемся по массиву
    // для каждого символа
    // создаем кнопку с помощью строкового литерала
    // записываем значение символа в атрибут "value" кнопки
    .map(symbol => {
        div.insertAdjacentHTML('beforeend', `<button value="${symbol}">${symbol}</button>`)
    })

Находим созданные кнопки и добавляем к ним обработчик события «клик»:

document.querySelectorAll('button').forEach(button => {
    button.addEventListener('click', function () {
        // по клику вызывается функция со значением кнопки в качестве параметра
        calc(this.value)
    })
})

Мы также хотим иметь возможность вводить символы с помощью клавиатуры. Для этого нам необходимо добавить обработчик события «нажатие клавиши» к объекту «Document» или «Window», затем отфильтровать ненужные значения свойства «ключ» клавиши, например, с помощью регулярного выражения:

document.addEventListener('keydown', event => {
    if ((event.key).match(/[0-9%\/*\-+\(\)=]|Backspace|Enter/)) calc(event.key)
})

Метод «match» в данном случае играет роль фильтра: он не позволяет передавать функции «calc» аргумент, не соответствующий заданному в нем условию.

Само условие звучит так: если значением event.key является один из символов, указанных в квадратных скобках ([]; цифра от 0 до 9, знаки деления, умножения, сложения, вычитания, открывающая, закрывающая круглые скобки или знак равенства; обратная косая черта — экранирование) или (| — альтерация) Backspace, или Enter, то вызываем calc с event.key в качестве параметра, иначе ничего не делаем (Shift также успешно отбрасывается).

Наша главная (и единственная) функция «calc» выглядит следующим образом (код следует читать снизу вверх):

// функция принимает значение кнопки или ключ клавиши
function calc(value) {
    // если нажат знак равенства или Enter
    if (value.match(/=|Enter/)) {
        // пробуем выполнить операцию
        try {
            // вычисляем значение строки
            // это возможно благодаря методу "evaluate" объекта "math"
            // Math.trunc используется для округления до целого числа
            output.textContent = Math.trunc(math.evaluate(output.textContent))
            
        // если операцию выполнить невозможно
        } catch {
            // сохраняем значение поля
            let oldValue = output.textContent
            // создаем новую переменную
            let newValue = 'недопустимое выражение'
            // выводим значение новой переменной в поле
            output.textContent = newValue
            // через полторы секунды возвращаем полю старое значение
            setTimeout(() => {
                output.textContent = oldValue
            }, 1500)
        }
        
    // если нажат символ "C"
    } else if (value === 'C') {
        // очищаем поле
        output.textContent = ''
    
    // если нажат символ "СЕ" или Backspace
    } else if (value.match(/CE|Backspace/)) {
        // уменьшаем строку на один символ
        output.textContent = output.textContent.substring(0, output.textContent.length - 1)
        
    // если нажата любая другая (отфильтрованная) кнопка или клавиша
    } else {
        // записываем ее значение в поле
        output.textContent += value
    }
}

В завершение, парочка слов о заявленной масштабируемости и полнофункциональности.

Метод «evaluate» (ранее «eval») и другие методы Math.js имеют очень большие возможности. Опираясь на эти возможности, мы можем легко расширить функционал нашего калькулятора, добавив в него новые символы и операторы, предусмотрев возможность работы с числами с плавающей точкой (регулируя количество знаков после запятой с помощью переключателя и метода «toFixed») и т.д.

Результат:



Код на GitHub.

Благодарю за внимание. Надеюсь, вы нашли для себя что-то полезное. Хороших выходных и счастливого кодинга.

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 13

    +4
    1. Регулярки тут не нужны, они намного медленней switch-case.
    2. addEventListner нужно цеплять на родительский элемент и потом вычислять кнопку через event.target, чтобы не создавать столько мусора в памяти.
    3. Генерацию клавиатуры нужно делать одним махом, вместо дёрганий по одной кнопке.

    Это при беглом просмотре очень режет глаз. Вообще, код довольно неплох для начинающего JS-дева.


    ps. минус не мой

      0

      Вообще очень советую выучить конструкцию switch-case и самые обычные indexOf.


      let oldValue => const
      let newValue => const

        +1

        const oldValue = output.textContent;


        output.textContent = "недопустимое выражение";
        setTimeout(() => {
        output.textContent = oldValue;
        }, 1500);


        Таймаут нужно сохранять в переменную и делать clearTimeout + возврат старого значения перед каждым новым выполнением.

      0
      document.querySelectorAll('button').forEach(button => {
      button.addEventListener('click', function () {
      // по клику вызывается функция со значением кнопки в качестве параметра
      calc(this.value)
      })
      })


      Не стоит так делать в случае, если элементов много, это не производительно.
        0

        Как посчитать производительность?

          0
          Измерением времени на выполнение операций.
          Если просто то так:
          1. Записываете в переменную const startTime = performance.now()
          2. Выполняете операции
          3. Смотрите на performance.now() — startTime

          habr.com/ru/company/tinkoff/blog/489230
          Вот тут можете найти еще немного информации по измерению производительности в браузере
        0
        Стили для * { ... } не самая производительная вещь. Более того при таких стилях становится очень сложно переиспользовать вёрстку на других проектах и использовать сторонние библиотеки. В одном проекте * { box-sizing: border-box; } сломал встроенные гуглокарты, причём не сразу, а после их какого-то обновления.

        Рекомендую почитать про БЭМ, подход сильно упрощает разработку.
          0
          Это шедевр, ящитаю. Такие произведения надо сохранять в камне и металле, в назидание потомкам.
            +3
            Наш калькулятор будет true simple (42 строки кода, включая пробелы между блоками), но при этом полнофункциональным и масштабируемым.
            и 1,79МБ подключаемой библиотеки math.js (550кБ после её пережатия).
              +1
              Это пока уровень 5-8 класса школы, нужно двигаться дальше — написать свой парсер простых математических выражений, с функциями, скобками и переменными. Это уже будет курсовая уровня 1 курса.
                0

                Было бы неплохо этот калькулятор сделать таким одним файлом html, который ничего с инета не качает. Тогда можно его прям по скайпу кинуть и использовать на любом компе всегда в одном первозданном виде, независимо от версии ОС.

                  +1
                  И даже одним URL ом можно :) kod.ru/itty-bitty-site
                    0

                    Забавно. Я делал точно так же: в строке URL хранил zip -> base64 закодированные данные. Видимо, не одного меня адресная строка привлекает.


                    Мой пример


                    Беда лишь в том, что на многих сервисах есть ограничения по длине строки. Например, на ГитХабе можно всунуть 8К символов, а на БитБакете лишь 4К. Хабр так совсем обрезает рано. Хранить такие ссылки без сократителей ссылок тоже тяжко.

                Only users with full accounts can post comments. Log in, please.