/** * Проверяет, является ли строка формулой * @param {string} value - Значение ячейки * @returns {boolean} */ export const isFormula = (value) => { if (typeof value !== 'string') return false; return value.startsWith('='); }; export const normalizeNumericInput = (value) => { if (typeof value !== 'string') return value; const trimmed = value.trim(); if (!/^[+-]?(?:\d+(?:[.,]\d*)?|[.,]\d+)$/.test(trimmed)) return value; return trimmed.replace(',', '.'); }; /** * Извлекает формулу из строки (удаляет знак =) * @param {string} value - Значение ячейки * @returns {string} - Формула без знака = */ export const extractFormula = (value) => { if (!isFormula(value)) return value; return value.substring(1).trim(); }; /** * Вычисляет математическое выражение * Поддерживает: +, -, *, /, %, скобки * @param {string} expression - Математическое выражение * @returns {number} - Результат вычисления * @throws {Error} - Если выражение некорректно */ export const calculateFormula = (expression) => { try { // Удаляем пробелы и приводим десятичную запятую к точке const cleanExpr = expression.replace(/\s/g, '').replace(/,/g, '.'); // Проверяем на допустимые символы (только цифры, операторы, скобки, точка) const validChars = /^[\d+\-*/%().]+$/; if (!validChars.test(cleanExpr)) { throw new Error('Недопустимые символы в формуле'); } // Заменяем % на /100 для корректного вычисления процентов const exprWithPercent = cleanExpr.replace(/%/g, '/100'); // Безопасное вычисление с использованием Function // ВНИМАНИЕ: Это безопасно, так как мы проверяем допустимые символы const result = new Function(`return (${exprWithPercent})`)(); // Проверяем, что результат - число if (typeof result !== 'number' || !isFinite(result)) { throw new Error('Некорректный результат вычисления'); } // Округляем до 10 знаков после запятой для избежания проблем с плавающей точкой return Math.round(result * 1e10) / 1e10; } catch (error) { console.error('Formula calculation error:', error); throw new Error(`Ошибка вычисления: ${error.message}`); } }; /** * Вычисляет значение ячейки, если это формула * @param {string} value - Значение ячейки * @returns {string|number} - Вычисленное значение или исходное значение */ export const evaluateCellValue = (value) => { if (!isFormula(value)) return value; try { const formula = extractFormula(value); const result = calculateFormula(formula); return result; } catch (error) { console.error('Error evaluating cell:', error); return '#ОШИБКА'; } }; /** * Начальное состояние редактора ячейки. * Решает, показывать ли сохранённую формулу или текущее значение ячейки, * и помечает формулу как устаревшую (stale), если её результат расходится * с текущим значением — значит, значение изменил кто-то другой, либо * формула больше не вычисляется. * @param {string|null} savedFormula - строка вида '=...' из localStorage, либо null * @param {string|number|null} value - текущее значение ячейки (с сервера) * @returns {{ currentValue: string|number|null, isFormulaMode: boolean, stale: boolean }} */ export const resolveEditorState = (savedFormula, value) => { if (!savedFormula || !isFormula(savedFormula)) { return { currentValue: value, isFormulaMode: false, stale: false }; } try { const expression = extractFormula(savedFormula); const result = calculateFormula(expression); const numericValue = Number(value); const stale = Number.isNaN(numericValue) || Math.abs(result - numericValue) > 1e-9; if (stale) { return { currentValue: value, isFormulaMode: false, stale: true }; } return { currentValue: savedFormula, isFormulaMode: true, stale: false }; } catch { return { currentValue: value, isFormulaMode: false, stale: true }; } }; const isEmptyCellValue = (value) => value === null || value === undefined || value === ''; const isNumericCellValue = (value) => !isEmptyCellValue(value) && !Number.isNaN(Number(value)); /** * Сравнивает два значения ячейки так, как их видит пользователь: * числа сравниваются как числа ('150' == 150, '150.0' == 150), '' == null. * @param {string|number|null} a * @param {string|number|null} b * @returns {boolean} */ export const isSameCellValue = (a, b) => { const aEmpty = isEmptyCellValue(a); const bEmpty = isEmptyCellValue(b); if (aEmpty || bEmpty) return aEmpty && bEmpty; if (isNumericCellValue(a) && isNumericCellValue(b)) { return Number(a) === Number(b); } return a === b; }; /** * Форматирует число для отображения * @param {number} value - Число для форматирования * @returns {string} - Отформатированная строка */ export const formatNumber = (value) => { if (typeof value !== 'number') return String(value); // Если число целое, отображаем без десятичной части if (Number.isInteger(value)) { return value.toString(); } // Иначе показываем до 10 знаков, но убираем лишние нули return value.toFixed(10).replace(/\.?0+$/, ''); };