Приветствие
Доброго времени суток, уважаемый Хабр. Меня зовут Илья и это моя первая статья. Откровенно говоря, я давно уже искал повод опубликовать какой-нибудь материал и вот, похоже этот день настал. Сразу предупрежу - я не считаю решение описанное ниже какой-то метой или истинно верным, но когда вам нужно срочно посчитать строку содержащую простые математические выражения, с использованием скобок, оно может сильно упростить жизнь.
Предисловие
Примерно неделю назад я столкнулся с необходимостью преобразовать строку подобного типа (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
Спасибо за внимание, понимаю, что данный инструмент просто велосипед, но имеющий право на существование.
Комментарии (19)
Alexandroppolus
00.00.0000 00:00+1Я бы не назвал этот вариант "простейшим". Есть же способ через new Function(...), с дополнительными плюшками.
iliks Автор
00.00.0000 00:00соглашусь и возможно даже это будет более быстрым решением, но для моей задачи требовалось отказаться и от eval и от Function.
Данный код гуляет на просторах интернета и вполне рабочий способ
const rgx = /(?:(?:^|[-+_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-]?\d+)?\s*))+$/; function parse(str) { if (!rgx.test(str)) return 'invalid entry!' return Function('use strict'; return (<span class="hljs-subst">${str}</span>))() }
Bigata
00.00.0000 00:00+6Что это такое я сейчас прочитал?
xi-tauw
00.00.0000 00:00+12Статью, где человек думал, что обманул систему, а на самом деле обманул себя.
mpaxepnepe
00.00.0000 00:00-1фильм вычислитель - сноуден в главной роли:
????????: - Хочу, что бы у меня все было.
????????: - У тебя все было
venanen
00.00.0000 00:00Как говорил Дью, я вот знаю как. Я не знаю зачем. Regexp для валидации инпута и new Function и дело в шляпе.
iliks Автор
00.00.0000 00:00Мне необходимо проводить порядка 100т. операций или даже больше, в таком случае Function не будет особо приятным решением
15118 - время 50т. операций через Function()
1154 - время 50т. операций через calc()
В среднем моё решение в 8 раз быстрее
venanen
00.00.0000 00:00+5Вы покажите, как вы считаете это время. Потому что я никогда не поверю, что вызов ванильной функции js в 8 раз медленнее, чем изменение css и потом его чтение.
kachurun
00.00.0000 00:00Звучит как полный бред, лучшее е привезите примеры кода который вы сравнивали. Невозможно поверить что создание элемента и просчет его стиля быстрее чем просто выполнение функции.
Irval
00.00.0000 00:00+1Очень странный подход, однако. Очевидно, что если Вам важна скорость написания кода, то использовать стоит eval. Он и работать должен быстрее, чем предложенный в статье метод. Если хотите предотвратить возникновение уязвимостей и считать все так же быстро - напишите алгоритм Дейкстры «Сортировочная станция» для перевода в польскую обратную запись. На Хабре об этом уже писали - https://habr.com/ru/post/100869/.
romcky
00.00.0000 00:00+1Ну вот, ни рекурсивного спуска тебе, ни обратной польской нотации, калькулятор спрограммировал называется
light_and_ray
00.00.0000 00:00Ну хотя бы не стена кода на реализацию ast-дерева, как на такое пошла почему-то мода
1e100
00.00.0000 00:00// chatGPT code :)
function calculateMathExpression(expr, ...args) {
const sanitizedExpr = expr.replace(/[^0-9+-*/().]/g, '');
const fnBody =return ${sanitizedExpr};
;
const fn = new Function(fnBody);
return fn(...args);
}
PaulZi
00.00.0000 00:00Чудес не бывает. Либо мы строим синтаксическое дерево, либо вызываем функцию которая это делает за нас)
xxxphilinxxx
TL;DR: таки eval, просто не тот eval, а CSS calc.