Pull to refresh

Простейшее решение калькулятора на js без eval();

Level of difficultyEasy
Reading time4 min
Views12K

Приветствие

Доброго времени суток, уважаемый Хабр. Меня зовут Илья и это моя первая статья. Откровенно говоря, я давно уже искал повод опубликовать какой-нибудь материал и вот, похоже этот день настал. Сразу предупрежу - я не считаю решение описанное ниже какой-то метой или истинно верным, но когда вам нужно срочно посчитать строку содержащую простые математические выражения, с использованием скобок, оно может сильно упростить жизнь.

Предисловие

Примерно неделю назад я столкнулся с необходимостью преобразовать строку подобного типа (81+47*(33-42))/15 в математическое выражение и посчитать его. Казалось бы используй любое решение из интернетов и дело в шляпе, но к сожалению использовать библиотеки нельзя, а решения которые я находил как правило считали не правильно, то порядок действий путают, то каким-то образом отрицательное число получается... В общем я просто написал свой код и был доволен, но на следующий день меня вдруг осенило.

Перейдем к практике

Я предлагаю рассмотреть все на практике написания калькулятора, который будет составлять строку а позже считать её, оговорюсь очень важно, чтоб число скобок в выражении было сколько нашей душе угодно и не так важно число символов после запятой, для моей задачи хватит и 3-х.

И так, напишем простой интерфейс с полем ввода и выводом результата

<div class="calc">
    <div class="story"></div>
    <input class="input" type="text" name="">
</div>

И добавим ему симпатичных стилей

body, input {padding: 0; margin: 0; border: 0; font-family: 'Roboto';}
body {
	background: #222; 
	color:#fff;
	display: flex;
	height: 100vh;
	justify-content: center;
	align-items: center;
}

.calc {
	height: 420px;
	width: 240px;
	position: relative;
	border-radius: 8px;
	box-shadow: -5px -5px 20px 5px rgba(255,255,255,0.08), 5px 5px 20px 5px rgba(0,0,0,0.5), -1px -1px 0px 0px rgba(255,255,255,0.2), 1px 1px 0px 0px rgba(0,0,0,0.4);
	background: linear-gradient(120deg, #464e55 0%, #292929 100%);
}

.calc:after {
	content: '';
	position: absolute;
	z-index: -1;
	background: #fff;
	display: block;
	width: calc(100% + 2px);
	height: calc(100% + 2px);
	margin: -1px;
	top: 0;
	border-radius: 8px;
	background: linear-gradient(120deg, #4f6579 0%, #312e40 100%);
}

.story {
	height: calc(420px - 42px);
	text-align: right;
	box-sizing: border-box;
	padding: 10px 20px;
	display: flex;
	justify-content: end;
	flex-direction: column;
	opacity: 0.6;
	overflow: hidden;
	position: relative;
}

.story:after {
	content: '';
	background: linear-gradient(180deg, #3e464d 0%, #312e4000 50%);
	display: block;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	position: absolute;
	border-radius: 10px;
}

.story i {
	font-size: 12px;
	font-style: normal;
}

.input {
	outline: none;
	border: none;
	margin: 0;
	width: 100%;
	background: none;
	color: #fff;
}

.input {
	height: 42px;
	border-top: 1px dotted #8096a4;
	box-sizing: border-box;
	text-align: right;
	padding: 0 20px;
	line-height: 40px;
	font-size: 18px;
	font-weight: 300;
}

А далее создадим маленькую магии и напишем простой код для создания строки, которую мы будем считать

let string = '',
    $input = document.querySelector('.input');
    $story = document.querySelector('.story');

// событие для отлавливания нажатия в поле input
$input.onkeyup = function(e){
    // если нажат Enter то считаем строку
	if(e.key == 'Enter'){
        // это наша будущая функция
		let result = strToMath(string); 
		
        // передаём результат в поле ввода чтоб иметь возможность дальше работать с ним
        this.value = result; 
		
        // просто сохраняем историю
        $story.innerHTML += `<i>${string}=${result}</i>`; 
		
        // а это вынужденный костыль, чтоб считать правильно дробные числа
        string = `(${string})`; 
	}
	else
		string += e.key; // не совсем верное решение но ведь это просто пример
}

Хорошо, тут должно быть в целом все понятно, но что у нас с strToMath()?

А собственно вот и она

function strToMath(string){
    // преходящие значение необходимо разбить на массив по символам, иначе ничего работать не будет
	string = string.replaceAll(' ', '').replaceAll('+', ' + ').replaceAll('*', ' * ').replaceAll('-', ' - ').replaceAll('/', ' / ').split(' ');

    // Переносим повторный символ == "-" к следующему числу
	for(let i = 0; i < string.length; i++){
		if(string[i] == ''){
			string.splice(i, 2);
			string[i] = '-'+string[i];
		}
	}

    // а теперь та самая магичиская и потрясающая функция calc() прямиком из css3
    // создаем любой элемент
	let calc = document.createElement('calc');

    // передаём туда в свойство opacity наш объедененный через пробел массив 
	calc.style['opacity'] = `calc(${string.join(' ')})`;

    // получаем значение обратно удаляя все лишнее
	let result = parseFloat(calc.style['opacity'].replace('calc(', '').replace(')', ''))

    // и удаляем сам элемент
	calc.remove();
  
    // результат возвращаем.
	return result;
}

Грубо говоря, данное решение в 4 строки если поступающие данные всегда валидны.

По большому счету мы просто эксплуатируем функцию из css3 для получения решения. Однако есть и минусы, если у нас калькулятор то прошлое решение должно поступать с новым в данную функцию т.к. calc() обрезает результат до 5-ти символов после запятой и при делении 89/81 = 1.09877 после обратного умножения мы не получим 89 , результатом будет: 89.0004

Еще говоря об opacity, если использовать z-index то значение будет округляться, а размерные значения требуют обязательно в конце умножения на 1px т.е. примерно так будет выглядеть код:

calc.style['top'] = `calc((${string.join(' ')}) * 1px)`;

let result = parseFloat(calc.style['top'].replace('calc(', '').replace('px)', ''))

Так же собрал маленькое демо с полноценным калькулятором доступным по ссылке: https://preview-1326290.playcode.io/

Код демки: https://playcode.io/1326290

Спасибо за внимание, понимаю, что данный инструмент просто велосипед, но имеющий право на существование.

Tags:
Hubs:
+3
Comments21

Articles