Files
QWERTYkez.OpenXmlProcessors/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs
melekhin f5eb667973 0.9.1
2026-06-08 14:31:31 +07:00

775 lines
25 KiB
C#
Raw 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="ICell"/>.
/// </summary>
internal sealed class ExcelCell : ICell
{
private readonly ExcelWriter _writer;
private readonly ExcelSheet _sheet;
private uint _row;
private uint _col;
internal ExcelCell(ExcelWriter writer, ExcelSheet sheet, uint row, uint col)
{
_writer = writer;
_sheet = sheet;
_row = row;
_col = col;
}
public bool IsMerged
{
get
{
var range = GetMergedRange();
return range != null;
}
}
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 range))
{
if (_row >= range.RowStart && _row <= range.RowEnd &&
_col >= range.ColStart && _col <= range.ColEnd)
return range;
}
}
return null;
}
}
public bool TryGetMergedRange(IRange range)
{
var merged = GetMergedRange();
if (merged == null) return false;
return merged.Equals(range);
}
public uint Row => _row;
public uint Col => _col;
public string ColLetter => CellAddressHelper.ColumnIndexToLetter(_col);
public ICell MoveTo(uint rowIndex, uint colIndex)
{
if (rowIndex == _row && colIndex == _col) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
// Вырезать-вставить: копируем данные в новую позицию, очищаем старую
var srcCell = GetCellElement();
if (srcCell != null)
{
var cloned = (Cell)srcCell.CloneNode(true);
InsertCellAt(rowIndex, colIndex, cloned);
srcCell.Remove();
}
_row = rowIndex;
_col = colIndex;
}
return this;
}
public ICell MoveTo(uint rowIndex, string colIndex) => MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex));
public RowHeight Height
{
get => ExcelRow.GetHeight(_writer, _sheet, _row);
set => ExcelRow.SetHeight(_writer, _sheet, _row, value);
}
public ColumnWidth Width
{
get => ExcelColumn.GetWidth(_writer, _sheet, _col);
set => ExcelColumn.SetWidth(_writer, _sheet, _col, value);
}
public bool IsNumber
{
get
{
var cell = GetCellElement();
if (cell == null) return false;
if (cell.DataType != null && cell.DataType.Value == CellValues.Number) return true;
// Если тип не указан, но значение число Excel считает числом
if (cell.DataType == null && cell.CellValue != null && double.TryParse(cell.CellValue.Text, out _)) return true;
return false;
}
}
public bool IsBoolean
{
get
{
var cell = GetCellElement();
return cell != null && cell.DataType != null && cell.DataType.Value == CellValues.Boolean;
}
}
public bool IsError
{
get
{
var cell = GetCellElement();
return cell != null && cell.DataType != null && cell.DataType.Value == CellValues.Error;
}
}
public bool IsDate
{
get
{
if (!IsNumber) return false;
var format = GetNumberFormat();
if (format == null) return false;
// Встроенный формат: проверяем по ID
if (format.Id.HasValue && format.Id.Value < 164)
{
int id = format.Id.Value;
return (id >= 14 && id <= 22) || // даты
(id >= 27 && id <= 36) || // время и дата-время
(id >= 45 && id <= 47); // другие форматы времени
}
else
{
// Пользовательский формат: ищем символы даты/времени
string code = format.Format?.ToLowerInvariant() ?? string.Empty;
bool isDate = code.Contains('d') && code.Contains('m') && code.Contains('y');
bool isTime = code.Contains('h') && code.Contains('m') && code.Contains('s');
return isDate || isTime;
}
}
}
public bool IsLocked
{
get
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return false;
return _writer.IsCellLocked(styleIndex);
}
}
public bool HasFormula => GetCellElement()?.CellFormula != null;
public string GetString()
{
var cell = GetCellElement();
if (cell == null) return string.Empty;
if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString)
{
// Общая таблица строк
if (uint.TryParse(cell.CellValue?.Text, out uint idx))
return GetSharedString(idx);
return string.Empty;
}
if (cell.DataType != null && cell.DataType.Value == CellValues.InlineString)
{
// Rich text извлекаем весь текст
return ExtractTextFromInlineString(cell.InlineString);
}
if (cell.DataType != null && cell.DataType.Value == CellValues.Boolean)
return cell.CellValue?.Text ?? "FALSE";
if (cell.DataType != null && cell.DataType.Value == CellValues.Error)
return cell.CellValue?.Text ?? "#NULL!";
// Число или общий тип
return cell.CellValue?.Text ?? string.Empty;
}
/// <inheritdoc />
public NumberFormatPattern? GetNumberFormat()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return null;
return _writer.GetNumberFormat(styleIndex);
}
/// <inheritdoc />
public CellAlign GetCellAlign()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return default;
return _writer.GetCellAlign(styleIndex);
}
/// <inheritdoc />
public CellBorder GetCellBorder()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return default;
return _writer.GetCellBorder(styleIndex);
}
/// <inheritdoc />
public CellFill GetCellFill()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return default;
return _writer.GetCellFill(styleIndex);
}
/// <inheritdoc />
public CellFont GetCellFont()
{
var cell = GetCellElement();
if (cell?.StyleIndex?.Value is not uint styleIndex)
return default;
return _writer.GetCellFont(styleIndex);
}
public bool TryGetBoolean(out bool value)
{
value = false;
var cell = GetCellElement();
if (cell == null) return false;
if (cell.DataType != null && cell.DataType.Value == CellValues.Boolean)
{
string txt = cell.CellValue?.Text ?? "0";
value = txt == "1" || txt.Equals("true", StringComparison.OrdinalIgnoreCase);
return true;
}
return false;
}
public bool? GetBoolean() => TryGetBoolean(out bool v) ? v : null;
public bool TryGetDate(out DateTime value)
{
value = default;
if (!IsNumber) return false;
if (TryGetNumber(out double num))
{
// Excel даты: 1 января 1900 = 1, 1 января 1904 = 0 в 1904 системе
// Предполагаем 1900 систему
value = DateTime.FromOADate(num);
return true;
}
return false;
}
public DateTime? TryGetDate() => TryGetDate(out DateTime d) ? d : null;
public bool TryGetNumber(out double value)
{
value = 0;
var cell = GetCellElement();
if (cell == null) return false;
if (cell.DataType != null && cell.DataType.Value == CellValues.Number)
{
if (cell.CellValue != null && double.TryParse(cell.CellValue.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
return true;
}
else if (cell.DataType == null && cell.CellValue != null && double.TryParse(cell.CellValue.Text, NumberStyles.Any, CultureInfo.InvariantCulture, out value))
return true;
return false;
}
public double? TryGetNumber() => TryGetNumber(out double v) ? v : null;
public bool TrySet(string formula, NumberFormatPattern? format = null)
{
if (string.IsNullOrEmpty(formula)) return false;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
cell.CellFormula = new CellFormula(formula);
cell.DataType = null; // формула сама определяет тип
if (format != null)
SetNumberFormatInternal(cell, format);
return true;
}
}
public ICell Set(string formula, NumberFormatPattern? format = null)
{
if (!TrySet(formula, format))
throw new InvalidOperationException("Failed to set formula");
return this;
}
public ICell Set(NumberFormatPattern format)
{
if (format == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
SetNumberFormatInternal(cell, format);
}
return this;
}
/// <inheritdoc />
public ICell Set(CellAlign align)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
int styleIndex = _writer.GetOrCreateCellFormatId(
numberFormat: null,
font: null,
fill: null,
border: null,
align: align
);
cell.StyleIndex = (uint)styleIndex;
return this;
}
}
/// <inheritdoc />
public ICell Set(CellBorder border)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
int styleIndex = _writer.GetOrCreateCellFormatId(
numberFormat: null,
font: null,
fill: null,
border: border,
align: null
);
cell.StyleIndex = (uint)styleIndex;
return this;
}
}
/// <inheritdoc />
public ICell Set(CellFill fill)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
int styleIndex = _writer.GetOrCreateCellFormatId(
numberFormat: null,
font: null,
fill: fill,
border: null,
align: null
);
cell.StyleIndex = (uint)styleIndex;
return this;
}
}
/// <inheritdoc />
public ICell Set(CellFont font)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
int styleIndex = _writer.GetOrCreateCellFormatId(
numberFormat: null,
font: font,
fill: null,
border: null,
align: null
);
cell.StyleIndex = (uint)styleIndex;
return this;
}
}
public ICell Text(Action<ICellText> value)
{
if (value == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
var textObj = new ExcelCellText();
value(textObj);
cell.InlineString = BuildInlineString(textObj);
cell.DataType = CellValues.InlineString;
cell.CellValue = null;
cell.CellFormula = null;
// Если есть разрыв строки (символ \n), включаем перенос текста для ячейки
bool hasNewline = textObj.GetRuns().Any(r => r.Text != null && r.Text.Contains('\n'));
if (hasNewline)
{
var currentAlign = GetCellAlign();
if (currentAlign.WrapText != true)
{
var newAlign = new CellAlign
{
Horizontal = currentAlign.Horizontal,
Vertical = currentAlign.Vertical,
WrapText = true,
ShrinkToFit = currentAlign.ShrinkToFit
};
Set(newAlign);
}
}
}
return this;
}
/// <inheritdoc />
public ICellText? Text()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetCellElement();
if (cell == null || cell.DataType?.Value != CellValues.InlineString || cell.InlineString == null)
return null;
var textObj = new ExcelCellText();
foreach (var run in cell.InlineString.Elements<Run>())
{
string text = run.GetFirstChild<Text>()?.Text ?? string.Empty;
RunFormat? format = null;
if (run.RunProperties != null)
{
// Преобразуем RunProperties в RunFormat (можно вынести в отдельный метод)
format = RunFormat.FromRunProperties(run.RunProperties);
}
textObj.AddRun(text, format);
}
return textObj;
}
}
/// <inheritdoc />
public bool TryText(out ICellText cellText)
{
cellText = Text()!;
return cellText is not null;
}
public ICell Set(string value)
{
if (value == null) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
// Проверяем, можно ли сохранить как число?
if (double.TryParse(value, out double num))
{
cell.DataType = CellValues.Number;
cell.CellValue = new CellValue(num.ToString());
}
else
{
// Используем общую таблицу строк
int idx = GetOrAddSharedString(value);
cell.DataType = CellValues.SharedString;
cell.CellValue = new CellValue(idx.ToString());
}
cell.InlineString = null;
cell.CellFormula = null;
}
return this;
}
public ICell Set(bool value)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
cell.DataType = CellValues.Boolean;
cell.CellValue = new CellValue(value ? "1" : "0");
cell.InlineString = null;
cell.CellFormula = null;
}
return this;
}
public ICell Set(DateTime value, NumberFormatPattern? format = null)
{
double oa = value.ToOADate();
Set(oa, format);
return this;
}
public ICell Set(decimal value, NumberFormatPattern? format = null) => Set((double)value, format);
public ICell Set(double value, NumberFormatPattern? format = null)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetOrCreateCellElement();
cell.DataType = CellValues.Number;
cell.CellValue = new CellValue(value.ToString(CultureInfo.InvariantCulture));
if (format != null)
SetNumberFormatInternal(cell, format);
}
return this;
}
public ICell Set(float value, NumberFormatPattern? format = null) => Set((double)value, format);
public ICell Set(int value, NumberFormatPattern? format = null) => Set((double)value, format);
public ICell Set(long value, NumberFormatPattern? format = null) => Set((double)value, format);
public void ClearContent()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetCellElement();
if (cell != null)
{
cell.CellValue = null;
cell.CellFormula = null;
cell.InlineString = null;
cell.DataType = null;
}
}
}
public void ClearFormat()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var cell = GetCellElement();
cell?.StyleIndex = null;
}
}
public void Clear()
{
ClearContent();
ClearFormat();
}
public ICell CopyTo(uint rowIndex, uint colIndex, out ICell copiedCell)
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var srcCell = GetCellElement();
if (srcCell != null)
{
var cloned = (Cell)srcCell.CloneNode(true);
InsertCellAt(rowIndex, colIndex, cloned);
copiedCell = new ExcelCell(_writer, _sheet, rowIndex, colIndex);
}
else
{
copiedCell = new ExcelCell(_writer, _sheet, rowIndex, colIndex);
}
return this;
}
}
public ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell) =>
CopyTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex), out copiedCell);
// Private helpers
private Cell? GetCellElement()
{
var sheetData = _sheet.GetSheetData();
var row = FindRowElement(sheetData, _row);
if (row == null) return null;
return FindCellInRow(row, _col);
}
private Cell GetOrCreateCellElement()
{
var sheetData = _sheet.GetSheetData();
var row = GetOrCreateRowElement(sheetData, _row);
var cell = FindCellInRow(row, _col);
if (cell != null) return cell;
cell = new Cell();
string cellRef = CellAddressHelper.ColumnIndexToLetter(_col) + _row.ToString();
cell.CellReference = cellRef;
InsertCellInRow(row, cell, _col);
return cell;
}
private static Row? FindRowElement(SheetData sheetData, uint rowIndex)
{
foreach (var row in sheetData.Elements<Row>())
if (row.RowIndex?.Value == rowIndex) return row;
return null;
}
private static 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;
}
private 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);
}
private static 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;
}
private static void InsertCellInRow(Row row, Cell cell, uint colIndex)
{
string newRef = CellAddressHelper.ColumnIndexToLetter(colIndex) + (row.RowIndex?.Value ?? 1).ToString();
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);
}
private void InsertCellAt(uint rowIndex, uint colIndex, Cell cell)
{
var sheetData = _sheet.GetSheetData();
var row = GetOrCreateRowElement(sheetData, rowIndex);
InsertCellInRow(row, cell, colIndex);
}
private void SetNumberFormatInternal(Cell cell, NumberFormatPattern format)
{
if (format == null) return;
int styleIndex = _writer.GetOrCreateCellFormatId(
numberFormat: format,
font: null,
fill: null,
border: null,
align: null
);
cell.StyleIndex = (uint)styleIndex;
}
private string GetSharedString(uint index)
{
return _writer.GetSharedString(index);
}
private int GetOrAddSharedString(string value)
{
return _writer.GetOrAddSharedString(value);
}
private string ExtractTextFromInlineString(InlineString? inlineString)
{
if (inlineString == null) return string.Empty;
var sb = new System.Text.StringBuilder();
foreach (var run in inlineString.Elements<Run>())
{
var text = run.GetFirstChild<Text>()?.Text ?? string.Empty;
sb.Append(text);
}
return sb.ToString();
}
private InlineString BuildInlineString(ExcelCellText textObj)
{
var inline = new InlineString();
foreach (var run in textObj.GetRuns())
{
var runElement = new Run();
// Всегда создаём элемент Text, даже если строка состоит из "\n"
runElement.Append(new Text(run.Text));
if (run.Format is { } fmt)
{
var rPr = new RunProperties();
if (fmt.IsBold == true) rPr.Append(new Bold());
if (fmt.IsItalic == true) rPr.Append(new Italic());
if (fmt.Underline.HasValue)
{
rPr.Append(new Underline
{
Val = fmt.Underline.Value switch
{
UnderlineStyle.Single => UnderlineValues.Single,
UnderlineStyle.Double => UnderlineValues.Double,
UnderlineStyle.SingleAccounting => UnderlineValues.SingleAccounting,
UnderlineStyle.DoubleAccounting => UnderlineValues.DoubleAccounting,
_ => throw new NotImplementedException(),
}
});
}
if (fmt.IsStrike == true) rPr.Append(new Strike());
if (fmt.Color.HasValue)
{
var excelColor = fmt.Color.Value.ToExcel();
rPr.Append(excelColor);
}
if (fmt.FontSize.HasValue)
rPr.Append(new FontSize { Val = fmt.FontSize.Value });
if (!string.IsNullOrEmpty(fmt.FontFamily))
rPr.Append(new RunFont { Val = fmt.FontFamily });
if (fmt.Vertical.HasValue)
{
var vertAlign = new VerticalTextAlignment
{
Val = fmt.Vertical.Value == VerticalTextRunAlignment.Superscript
? VerticalAlignmentRunValues.Superscript
: VerticalAlignmentRunValues.Subscript
};
rPr.Append(vertAlign);
}
runElement.RunProperties = rPr;
}
inline.Append(runElement);
}
return inline;
}
private MergeCells? GetMergeCells() =>
_sheet.Worksheet.GetFirstChild<MergeCells>();
private 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;
}
}