namespace QWERTYkez.ExcelProcessor; /// /// Внутренняя реализация . /// internal sealed class ExcelRange : IRange { internal readonly ExcelWriter _writer; internal readonly ExcelSheet _sheet; internal uint _rowStart, _rowEnd; internal uint _colStart, _colEnd; internal ExcelRange(ExcelWriter writer, ExcelSheet sheet, uint rowStart, uint colStart, uint rowEnd, uint colEnd) { _writer = writer; _sheet = sheet; _rowStart = rowStart; _rowEnd = rowEnd; _colStart = colStart; _colEnd = colEnd; } public uint RowStart => _rowStart; public uint RowEnd => _rowEnd; public uint ColStart => _colStart; public uint ColEnd => _colEnd; public string ColStartLetter => CellAddressHelper.ColumnIndexToLetter(_colStart); public string ColEndLetter => CellAddressHelper.ColumnIndexToLetter(_colEnd); public uint Rows => _rowEnd - _rowStart + 1; public uint Cols => _colEnd - _colStart + 1; public bool IsMerged { get { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { var mergeCells = GetMergeCells(); if (mergeCells == null) return false; foreach (var mergeCell in mergeCells.Elements()) { if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var refRange)) { if (refRange._rowStart == _rowStart && refRange._rowEnd == _rowEnd && refRange._colStart == _colStart && refRange._colEnd == _colEnd) return true; } } return false; } } } void ForEachCell(Action action) { for (uint r = _rowStart; r <= _rowEnd; r++) for (uint c = _colStart; c <= _colEnd; c++) { var cell = new ExcelCell(_writer, _sheet, r, c); action(cell); } } public IRange Set(NumberFormatPattern format) { if (format == null) return this; _writer.ThrowIfDisposed(); lock (_writer._syncLock) { ForEachCell(cell => cell.Set(format)); return this; } } public IRange Set(CellAlign align) { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { ForEachCell(cell => cell.Set(align)); return this; } } public IRange Set(CellBorder border) { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { ForEachCell(cell => cell.Set(border)); return this; } } public IRange Set(CellFill fill) { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { ForEachCell(cell => cell.Set(fill)); return this; } } public IRange Set(CellFont font) { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { ForEachCell(cell => cell.Set(font)); return this; } } public IRange Set(CellStyle style) { if (style == null) return this; _writer.ThrowIfDisposed(); lock (_writer._syncLock) { ForEachCell(cell => cell.Set(style)); return this; } } /// /// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить"). /// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место. /// /// Номер строки для нового верхнего левого угла. /// Номер столбца для нового верхнего левого угла. /// Тот же объект IRange с новыми координатами. public IRange MoveTo(uint newRow, uint newCol) { if (newRow == _rowStart && newCol == _colStart) return this; _writer.ThrowIfDisposed(); lock (_writer._syncLock) { uint newRowEnd = newRow + (_rowEnd - _rowStart); uint newColEnd = newCol + (_colEnd - _colStart); bool overlap = RangesOverlap(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, newRowEnd, newColEnd); bool horizontalShift = (newRow == _rowStart); bool verticalShift = (newCol == _colStart); if (!overlap) { CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, CopyOrder.Any); ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd); } else if (horizontalShift) { CopyOrder order = (newCol > _colStart) ? CopyOrder.RightToLeft : CopyOrder.LeftToRight; CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, order); ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd); } else if (verticalShift) { CopyOrder order = (newRow > _rowStart) ? CopyOrder.BottomToTop : CopyOrder.TopToBottom; CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, order); ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd); } else { // Диагональное перемещение – два последовательных сдвига MoveTo(_rowStart, newCol); MoveTo(newRow, _colStart); return this; } _rowStart = newRow; _rowEnd = newRowEnd; _colStart = newCol; _colEnd = newColEnd; return this; } } public IRange MoveTo(uint rowIndex, string colIndex) { return MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex)); } enum CopyOrder { Any, LeftToRight, RightToLeft, TopToBottom, BottomToTop } /// Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода. void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd, uint dstRowStart, uint dstColStart, CopyOrder order) { // Определяем все строки, которые могут понадобиться (исходные и целевые) var rowIndices = new HashSet(); for (uint r = srcRowStart; r <= srcRowEnd; r++) rowIndices.Add(r); uint dstRowEnd = dstRowStart + (srcRowEnd - srcRowStart); for (uint r = dstRowStart; r <= dstRowEnd; r++) rowIndices.Add(r); var rowsDict = GetRowDictionary(rowIndices); int rowStep = 1, colStep = 1; int rowStart = (int)srcRowStart, rowEnd = (int)srcRowEnd; int colStart = (int)srcColStart, colEnd = (int)srcColEnd; if (order == CopyOrder.BottomToTop) { rowStep = -1; rowStart = (int)srcRowEnd; rowEnd = (int)srcRowStart; } if (order == CopyOrder.RightToLeft) { colStep = -1; colStart = (int)srcColEnd; colEnd = (int)srcColStart; } for (int r = rowStart; (rowStep > 0 ? r <= rowEnd : r >= rowEnd); r += rowStep) { if (!rowsDict.TryGetValue((uint)r, out var srcRow)) continue; for (int c = colStart; (colStep > 0 ? c <= colEnd : c >= colEnd); c += colStep) { Cell? srcCell = FindCellInRowFast(srcRow, (uint)c); if (srcCell == null) continue; Cell cloned = (Cell)srcCell.CloneNode(true); uint dstRow = dstRowStart + (uint)(r - srcRowStart); uint dstCol = dstColStart + (uint)(c - srcColStart); cloned.CellReference = $"{CellAddressHelper.ColumnIndexToLetter(dstCol)}{dstRow}"; InsertCellAt(dstRow, dstCol, cloned); } } } /// Строит словарь строк для указанных индексов строк. Dictionary GetRowDictionary(HashSet rowIndices) { var dict = new Dictionary(); var sheetData = _sheet.GetSheetData(); foreach (var row in sheetData.Elements()) { if (row.RowIndex?.Value is uint idx && rowIndices.Contains(idx)) dict[idx] = row; } return dict; } /// Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке). Cell? FindCellInRowFast(Row row, uint colIndex) { foreach (var cell in row.Elements()) { if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex) return cell; } return null; } /// Проверяет, пересекаются ли два прямоугольных диапазона. static bool RangesOverlap(uint r1s, uint c1s, uint r1e, uint c1e, uint r2s, uint c2s, uint r2e, uint c2e) { return !(r1e < r2s || r2e < r1s || c1e < c2s || c2e < c1s); } /// Вставляет ячейку в указанную позицию, предварительно удаляя существующую. void InsertCellAt(uint row, uint col, Cell cell) { DeleteCellAt(row, col); var sheetData = _sheet.GetSheetData(); var rowElement = GetOrCreateRowElement(sheetData, row); InsertCellInRow(rowElement, cell, col); } /// Удаляет ячейку, если она существует. void DeleteCellAt(uint row, uint col) { var sheetData = _sheet.GetSheetData(); var rowElement = FindRowElement(sheetData, row); if (rowElement == null) return; var cell = FindCellInRow(rowElement, col); cell?.Remove(); } /// Получает или создаёт строку с указанным индексом. Row GetOrCreateRowElement(SheetData sheetData, uint rowIndex) { var existing = FindRowElement(sheetData, rowIndex); if (existing != null) return existing; var newRow = new Row { RowIndex = rowIndex }; InsertRowElement(sheetData, newRow, rowIndex); return newRow; } /// Вставляет ячейку в строку с сохранением порядка столбцов. void InsertCellInRow(Row row, Cell cell, uint colIndex) { string newRef = $"{CellAddressHelper.ColumnIndexToLetter(colIndex)}{row.RowIndex?.Value ?? 1}"; cell.CellReference = newRef; bool inserted = false; foreach (var existing in row.Elements().ToList()) { if (CellAddressHelper.TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) && existingCol > colIndex) { existing.InsertBeforeSelf(cell); inserted = true; break; } } if (!inserted) row.Append(cell); } /// Очищает данные (содержимое) в указанном диапазоне. void ClearRangeData(uint rowStart, uint colStart, uint rowEnd, uint colEnd) { for (uint r = rowStart; r <= rowEnd; r++) { var row = FindRowElement(_sheet.GetSheetData(), r); if (row == null) continue; for (uint c = colStart; c <= colEnd; c++) { var cell = FindCellInRow(row, c); cell?.Remove(); } } } public bool TryMerge() { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { if (IsMerged) return false; var worksheet = _sheet.Worksheet; var mergeCells = worksheet.GetFirstChild(); if (mergeCells == null) { mergeCells = new MergeCells(); worksheet.Append(mergeCells); } string refStr = $"{ColStartLetter}{RowStart}:{ColEndLetter}{RowEnd}"; mergeCells.Append(new MergeCell { Reference = refStr }); return true; } } public void Unmerge() { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { var mergeCells = GetMergeCells(); if (mergeCells == null) return; var toRemove = mergeCells.Elements() .FirstOrDefault(mc => TryParseRangeReference(mc.Reference?.Value ?? string.Empty, out var rng) && rng._rowStart == _rowStart && rng._rowEnd == _rowEnd && rng._colStart == _colStart && rng._colEnd == _colEnd); toRemove?.Remove(); } } public IRange? GetMergedRange() { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { var mergeCells = GetMergeCells(); if (mergeCells == null) return null; foreach (var mergeCell in mergeCells.Elements()) { if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var refRange)) { if (refRange._rowStart >= _rowStart && refRange._rowEnd <= _rowEnd && refRange._colStart >= _colStart && refRange._colEnd <= _colEnd) return refRange; } } return null; } } public bool TryGetMergedRange(IRange range) { var merged = GetMergedRange(); if (merged == null) return false; return merged.Equals(range); } /// /// Применяет стиль ко всем ячейкам диапазона. /// /// Индекс стиля для применения. /// Если true, создаёт недостающие ячейки (по умолчанию true). void ApplyStyleToRange(uint styleIndex, bool createIfMissing = true) { for (uint row = _rowStart; row <= _rowEnd; row++) { for (uint col = _colStart; col <= _colEnd; col++) { var cell = GetCellInternal(row, col); if (cell != null) { cell.StyleIndex = styleIndex; } else if (createIfMissing) { var newCell = new Cell { CellReference = $"{CellAddressHelper.ColumnIndexToLetter(col)}{row}", StyleIndex = styleIndex }; InsertCellAt(row, col, newCell); } } } } public IEnumerable Cells { get { for (uint r = _rowStart; r <= _rowEnd; r++) for (uint c = _colStart; c <= _colEnd; c++) yield return new ExcelCell(_writer, _sheet, r, c); } } public bool GetSubCell(uint row, uint col, out ICell cell) { uint globalRow = _rowStart + row - 1; uint globalCol = _colStart + col - 1; if (globalRow > _rowEnd || globalCol > _colEnd) { cell = default!; return false; } cell = new ExcelCell(_writer, _sheet, globalRow, globalCol); return true; } public bool GetSubCell(uint row, string col, out ICell cell) { uint colIndex = CellAddressHelper.ColumnLetterToIndex(col); return GetSubCell(row, colIndex, out cell); } public bool TryEditSubCell(uint row, uint col, Action edit) { if (GetSubCell(row, col, out var cell)) { edit(cell); return true; } return false; } public bool TryEditSubCell(uint row, uint col, string value) { if (GetSubCell(row, col, out var cell)) { cell.Set(value); return true; } return false; } public bool TryEditSubCell(uint row, uint col, bool value) { if (GetSubCell(row, col, out var cell)) { cell.Set(value); return true; } return false; } public bool TryEditSubCell(uint row, uint col, string formula, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.SetFormula(formula, format); return true; } return false; } public bool TryEditSubCell(uint row, uint col, DateTime value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, uint col, decimal value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, uint col, double value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, uint col, float value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, uint col, int value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, uint col, long value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, string col, Action edit) { if (GetSubCell(row, col, out var cell)) { edit(cell); return true; } return false; } public bool TryEditSubCell(uint row, string col, string value) { if (GetSubCell(row, col, out var cell)) { cell.Set(value); return true; } return false; } public bool TryEditSubCell(uint row, string col, bool value) { if (GetSubCell(row, col, out var cell)) { cell.Set(value); return true; } return false; } public bool TryEditSubCell(uint row, string col, string formula, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.SetFormula(formula, format); return true; } return false; } public bool TryEditSubCell(uint row, string col, DateTime value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, string col, decimal value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, string col, double value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, string col, float value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, string col, int value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public bool TryEditSubCell(uint row, string col, long value, NumberFormatPattern? format = null) { if (GetSubCell(row, col, out var cell)) { cell.Set(value, format); return true; } return false; } public void ClearContents() { ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd); } public void ClearFormats() { for (uint r = _rowStart; r <= _rowEnd; r++) for (uint c = _colStart; c <= _colEnd; c++) { var cell = GetCellInternal(r, c); cell?.StyleIndex = null; } } public void Clear() { ClearContents(); ClearFormats(); } public IRange CopyTo(uint rowIndex, uint colIndex, out IRange copiedRange) { _writer.ThrowIfDisposed(); lock (_writer._syncLock) { CopyData(_rowStart, _colStart, _rowEnd, _colEnd, rowIndex, colIndex); copiedRange = new ExcelRange(_writer, _sheet, rowIndex, colIndex, rowIndex + Rows - 1, colIndex + Cols - 1); return this; } } public IRange CopyTo(uint rowIndex, string colIndex, out IRange copiedRange) { uint col = CellAddressHelper.ColumnLetterToIndex(colIndex); return CopyTo(rowIndex, col, out copiedRange); } // Вспомогательные методы Cell? GetCellInternal(uint row, uint col) { var sheetData = _sheet.GetSheetData(); var rowElement = FindRowElement(sheetData, row); if (rowElement == null) return null; return FindCellInRow(rowElement, col); } void CopyData(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd, uint dstRowStart, uint dstColStart) { // Сохраняем все значения и форматы из исходного диапазона var cellsData = new List<(uint row, uint col, Cell cell)>(); for (uint r = srcRowStart; r <= srcRowEnd; r++) { for (uint c = srcColStart; c <= srcColEnd; c++) { var cell = GetCellInternal(r, c); if (cell != null) { var cloned = (Cell)cell.CloneNode(true); cellsData.Add((r - srcRowStart + dstRowStart, c - srcColStart + dstColStart, cloned)); } } } // Вставляем в новый диапазон foreach (var (row, col, clonedCell) in cellsData) { InsertCellAt(row, col, clonedCell); } // Копируем ширины столбцов for (uint c = srcColStart; c <= srcColEnd; c++) { var srcCol = GetColumnElementForIndex(c); if (srcCol?.Width is { } width) { uint targetCol = c - srcColStart + dstColStart; var dstCol = GetOrCreateColumnElementForIndex(targetCol); dstCol.Width = width; dstCol.CustomWidth = srcCol.CustomWidth; } } // Копируем высоты строк for (uint r = srcRowStart; r <= srcRowEnd; r++) { var srcRow = FindRowElement(_sheet.GetSheetData(), r); if (srcRow is { } row && row.Height?.Value is { } hei && row.CustomHeight?.Value == true) { uint targetRow = r - srcRowStart + dstRowStart; var dstRow = GetOrCreateRowElement(targetRow); dstRow.Height = hei; dstRow.CustomHeight = true; } } } Row GetOrCreateRowElement(uint rowIndex) { var existing = FindRowElement(_sheet.GetSheetData(), rowIndex); if (existing != null) return existing; var newRow = new Row { RowIndex = rowIndex }; InsertRowElement(_sheet.GetSheetData(), newRow, rowIndex); return newRow; } static Row? FindRowElement(SheetData sheetData, uint rowIndex) { foreach (var row in sheetData.Elements()) if (row.RowIndex?.Value == rowIndex) return row; return null; } static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex) { bool inserted = false; foreach (var existing in sheetData.Elements().ToList()) { if (existing.RowIndex?.Value > rowIndex) { existing.InsertBeforeSelf(row); inserted = true; break; } } if (!inserted) sheetData.Append(row); } Cell? FindCellInRow(Row row, uint colIndex) { foreach (var cell in row.Elements()) { if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex) return cell; } return null; } Column? GetColumnElementForIndex(uint col) { var worksheet = _sheet.Worksheet; var cols = worksheet.GetFirstChild(); if (cols == null) return null; foreach (Column c in cols.Elements()) if (c.Min?.Value is { } min && c.Max?.Value is { } max && min <= col && max >= col) return c; return null; } Column GetOrCreateColumnElementForIndex(uint col) { var existing = GetColumnElementForIndex(col); if (existing != null) return existing; var worksheet = _sheet.Worksheet; var cols = worksheet.GetFirstChild(); if (cols == null) { cols = new Columns(); worksheet.InsertAt(cols, 0); } var newCol = new Column { Min = col, Max = col, Width = 8.43, CustomWidth = true }; cols.Append(newCol); return newCol; } MergeCells? GetMergeCells() => _sheet.Worksheet.GetFirstChild(); bool TryParseRangeReference(string reference, out ExcelRange range) { range = null!; if (string.IsNullOrEmpty(reference)) return false; string[] parts = reference.Split(':'); if (parts.Length != 2) return false; if (!CellAddressHelper.TryParseCellReference(parts[0] + "1", out uint row1, out uint col1)) return false; if (!CellAddressHelper.TryParseCellReference(parts[1] + "1", out uint row2, out uint col2)) return false; uint rowStart = Math.Min(row1, row2), rowEnd = Math.Max(row1, row2); uint colStart = Math.Min(col1, col2), colEnd = Math.Max(col1, col2); range = new ExcelRange(_writer, _sheet, rowStart, colStart, rowEnd, colEnd); return true; } }