/** * Проверяет, является ли строка формулой * @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 '#ОШИБКА'; } }; // AURORA-1891: сравнение "было/стало" для handleSave (EditCell.jsx) — если // значение фактически не изменилось, cell_updated не шлём (иначе лишняя // запись в БД и в журнале аудита при открытии/закрытии ячейки без правки). // Формула — особый случай: даже текстуально совпадающая формула всегда // считается «изменённой», чтобы handleSave по-прежнему всегда её // пересчитывал и писал в localStorage (так было до этой задачи). const normalizeComparableValue = (value) => { if (value === null || value === undefined) return ''; if (typeof value === 'number') return String(value); if (typeof value !== 'string') return String(value); const trimmed = value.trim(); if (trimmed === '') return ''; // "1 500,0" -> "1500,0": обычный и неразрывный пробел между цифрами — // разделитель тысяч. normalizeNumericInput сам такие пробелы не убирает, // он только меняет десятичную запятую на точку. const withoutThousandsSeparators = trimmed.replace(/(\d)[  ](?=\d)/g, '$1'); const normalized = normalizeNumericInput(withoutThousandsSeparators); if (typeof normalized === 'string' && /^[+-]?\d+(\.\d+)?$/.test(normalized)) { const asNumber = Number(normalized); if (Number.isFinite(asNumber)) return String(asNumber); } return trimmed; }; /** * Сравнивает старое и новое значение ячейки «по смыслу», а не строго: число * и его строковое представление (в т.ч. с разделителем тысяч и запятой вместо * точки) считаются равными, как и пустая строка с null/undefined. Формула * (строка, начинающаяся с "=") никогда не считается неизменной. * @param {string|number|null|undefined} a * @param {string|number|null|undefined} b * @returns {boolean} */ export const isSameCellValue = (a, b) => { if (isFormula(a) || isFormula(b)) return false; return normalizeComparableValue(a) === normalizeComparableValue(b); }; /** * Начальное состояние редактора ячейки. * Решает, показывать ли сохранённую формулу или текущее значение ячейки, * и помечает формулу как устаревшую (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 }; } }; /** * Форматирует число для отображения * @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+$/, ''); };