import { MergedCell } from '@/type/template' import { MergedCellInfo } from './types' export function getColumnLabel(index: number): string { let label = '' let col = index while (col >= 0) { label = String.fromCharCode(65 + (col % 26)) + label col = Math.floor(col / 26) - 1 } return label } // Создает адрес ячейки из координат (например, A1, B2) export function createCellAddress(row: number, col: number): string { return getColumnLabel(col) + (row + 1) } // Создает квалифицированную ссылку на ячейку с указанием листа export function createQualifiedCellReference( sheetType: 'report' | 'calculations', row: number, col: number ): string { const sheetName = sheetType === 'report' ? 'L' : 'R' const cellAddress = createCellAddress(row, col) return `${sheetName}!${cellAddress}` } export function findMergedCellInfo( row: number, col: number, mergedCells: MergedCell[] ): MergedCellInfo { for (const mergedCell of mergedCells) { const { startRow, startCol, endRow, endCol } = mergedCell // Проверяем, попадает ли ячейка в диапазон объединенных ячеек if (row >= startRow && row <= endRow && col >= startCol && col <= endCol) { // Это главная ячейка (левая верхняя) const isMainCell = row === startRow && col === startCol return { isMerged: true, isMainCell, mergedCell, spans: { rowSpan: endRow - startRow + 1, colSpan: endCol - startCol + 1, }, } } } return { isMerged: false, isMainCell: false } } // Регулярные выражения для разных типов ссылок на ячейки const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+\b/g // SheetName!A1 const LOCAL_REF_RE = /\b[A-Z]+[0-9]+\b/g // A1, B2, etc. const RANGE_RE = /\b[A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // A1:B2, D5:D7, etc. const QUALIFIED_RANGE_RE = /\b[A-Za-z0-9_]+![A-Z]+[0-9]+:[A-Z]+[0-9]+\b/g // SheetName!A1:B2 // Интерфейс для ссылки на ячейку export interface CellReference { sheet: string | null // null для локальных ссылок address: string // например "A1" или "B2:C3" isRange: boolean startRow: number startCol: number endRow: number endCol: number } // Парсит адрес ячейки типа "A1" в координаты export function parseCellAddress( address: string ): { row: number; col: number } | null { const match = address.match(/^([A-Z]+)(\d+)$/) if (!match) return null const column = match[1] const row = parseInt(match[2], 10) // Конвертируем колонку в индекс let colIndex = 0 for (let i = 0; i < column.length; i++) { colIndex = colIndex * 26 + (column.charCodeAt(i) - 65 + 1) } colIndex -= 1 // Конвертируем в 0-based индекс return { row: row - 1, col: colIndex } // Конвертируем в 0-based индексы } // Парсит диапазон ячеек типа "A1:B2" export function parseCellRange(range: string): { startRow: number startCol: number endRow: number endCol: number } | null { const parts = range.split(':') if (parts.length !== 2) return null const start = parseCellAddress(parts[0]) const end = parseCellAddress(parts[1]) if (!start || !end) return null return { startRow: Math.min(start.row, end.row), startCol: Math.min(start.col, end.col), endRow: Math.max(start.row, end.row), endCol: Math.max(start.col, end.col), } } // Извлекает все ссылки на ячейки из формулы export function extractCellReferences(formula: string): CellReference[] { if (!formula.startsWith('=')) return [] const expr = formula.substring(1) // Убираем знак = const references: CellReference[] = [] // Функция для проверки, не является ли ссылка частью имени функции const isFunction = (ref: string): boolean => { const commonFunctions = [ 'SUM', 'AVERAGE', 'COUNT', 'MAX', 'MIN', 'IF', 'AND', 'OR', 'NOT', 'СУММ', 'СРЕДНЕЕ', 'СЧЁТ', 'МАКС', 'МИН', 'ЕСЛИ', ] return commonFunctions.includes(ref.toUpperCase()) } // 1. Обрабатываем квалифицированные диапазоны (SheetName!A1:B2) const qualifiedRangeMatches = expr.match(QUALIFIED_RANGE_RE) if (qualifiedRangeMatches) { for (const match of qualifiedRangeMatches) { const [sheetPart, rangePart] = match.split('!') const rangeCoords = parseCellRange(rangePart) if (rangeCoords) { references.push({ sheet: sheetPart, address: rangePart, isRange: true, ...rangeCoords, }) } } } // 2. Обрабатываем локальные диапазоны (A1:B2) let cleanExpr = expr // Удаляем уже обработанные квалифицированные диапазоны cleanExpr = cleanExpr.replace(QUALIFIED_RANGE_RE, '') const rangeMatches = cleanExpr.match(RANGE_RE) if (rangeMatches) { for (const match of rangeMatches) { const rangeCoords = parseCellRange(match) if (rangeCoords) { references.push({ sheet: null, address: match, isRange: true, ...rangeCoords, }) } } } // 3. Обрабатываем квалифицированные одиночные ссылки (SheetName!A1) const qualifiedMatches = expr.match(QUALIFIED_REF_RE) if (qualifiedMatches) { for (const match of qualifiedMatches) { const [sheetPart, cellPart] = match.split('!') const cellCoords = parseCellAddress(cellPart) if (cellCoords) { references.push({ sheet: sheetPart, address: cellPart, isRange: false, startRow: cellCoords.row, startCol: cellCoords.col, endRow: cellCoords.row, endCol: cellCoords.col, }) } } } // 4. Обрабатываем локальные одиночные ссылки (A1, B2) // Удаляем уже обработанные квалифицированные ссылки и диапазоны cleanExpr = expr .replace(QUALIFIED_RANGE_RE, '') .replace(QUALIFIED_REF_RE, '') .replace(RANGE_RE, '') const localMatches = cleanExpr.match(LOCAL_REF_RE) if (localMatches) { for (const match of localMatches) { // Пропускаем функции if (isFunction(match)) continue const cellCoords = parseCellAddress(match) if (cellCoords) { references.push({ sheet: null, address: match, isRange: false, startRow: cellCoords.row, startCol: cellCoords.col, endRow: cellCoords.row, endCol: cellCoords.col, }) } } } return references } // Интерфейс для подсвеченной ячейки export interface HighlightedCell { sheet: string // нормализованное имя листа ('L' или 'R') row: number col: number color: string // класс CSS для цвета подсветки } // Конвертирует ссылки на ячейки в подсвеченные ячейки export function convertReferencesToHighlights( references: CellReference[], currentSheetType: 'report' | 'calculations' ): HighlightedCell[] { const highlights: HighlightedCell[] = [] const colors = [ 'bg-blue-200 border-blue-400', 'bg-green-200 border-green-400', 'bg-purple-200 border-purple-400', 'bg-red-200 border-red-400', 'bg-yellow-200 border-yellow-400', 'bg-indigo-200 border-indigo-400', 'bg-pink-200 border-pink-400', 'bg-orange-200 border-orange-400', ] let colorIndex = 0 for (const ref of references) { let sheetName: string // Определяем имя листа if (ref.sheet) { // Квалифицированная ссылка if (ref.sheet === 'L' || ref.sheet.toLowerCase() === 'report') { sheetName = 'L' } else if ( ref.sheet === 'R' || ref.sheet.toLowerCase() === 'calculations' ) { sheetName = 'R' } else { // Неизвестный лист, пропускаем continue } } else { // Локальная ссылка - используем текущий лист sheetName = currentSheetType === 'report' ? 'L' : 'R' } const color = colors[colorIndex % colors.length] colorIndex++ // Добавляем все ячейки в диапазоне for (let row = ref.startRow; row <= ref.endRow; row++) { for (let col = ref.startCol; col <= ref.endCol; col++) { highlights.push({ sheet: sheetName, row, col, color, }) } } } return highlights }