Files
melekhin e373d4108a
All checks were successful
Publish NuGet packages / publish (push) Successful in 28s
many debugs
2026-06-19 15:06:40 +07:00

862 lines
27 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Внутренняя реализация <see cref="IRange"/>.
/// </summary>
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<MergeCell>())
{
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<ICell> 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;
}
}
/// <summary>
/// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить").
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
/// </summary>
/// <param name="newRow">Номер строки для нового верхнего левого угла.</param>
/// <param name="newCol">Номер столбца для нового верхнего левого угла.</param>
/// <returns>Тот же объект IRange с новыми координатами.</returns>
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
}
/// <summary>Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода.</summary>
void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd,
uint dstRowStart, uint dstColStart, CopyOrder order)
{
// Определяем все строки, которые могут понадобиться (исходные и целевые)
var rowIndices = new HashSet<uint>();
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);
}
}
}
/// <summary>Строит словарь строк для указанных индексов строк.</summary>
Dictionary<uint, Row> GetRowDictionary(HashSet<uint> rowIndices)
{
var dict = new Dictionary<uint, Row>();
var sheetData = _sheet.GetSheetData();
foreach (var row in sheetData.Elements<Row>())
{
if (row.RowIndex?.Value is uint idx && rowIndices.Contains(idx))
dict[idx] = row;
}
return dict;
}
/// <summary>Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке).</summary>
Cell? FindCellInRowFast(Row row, uint colIndex)
{
foreach (var cell in row.Elements<Cell>())
{
if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex)
return cell;
}
return null;
}
/// <summary>Проверяет, пересекаются ли два прямоугольных диапазона.</summary>
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);
}
/// <summary>Вставляет ячейку в указанную позицию, предварительно удаляя существующую.</summary>
void InsertCellAt(uint row, uint col, Cell cell)
{
DeleteCellAt(row, col);
var sheetData = _sheet.GetSheetData();
var rowElement = GetOrCreateRowElement(sheetData, row);
InsertCellInRow(rowElement, cell, col);
}
/// <summary>Удаляет ячейку, если она существует.</summary>
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();
}
/// <summary>Получает или создаёт строку с указанным индексом.</summary>
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;
}
/// <summary>Вставляет ячейку в строку с сохранением порядка столбцов.</summary>
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<Cell>().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);
}
/// <summary>Очищает данные (содержимое) в указанном диапазоне.</summary>
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<MergeCells>();
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<MergeCell>()
.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<MergeCell>())
{
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);
}
/// <summary>
/// Применяет стиль ко всем ячейкам диапазона.
/// </summary>
/// <param name="styleIndex">Индекс стиля для применения.</param>
/// <param name="createIfMissing">Если true, создаёт недостающие ячейки (по умолчанию true).</param>
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<ICell> 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<ICell> 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<ICell> 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<Row>())
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<Row>().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<Cell>())
{
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<Columns>();
if (cols == null) return null;
foreach (Column c in cols.Elements<Column>())
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<Columns>();
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<MergeCells>();
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;
}
}