namespace QWERTYkez.ExcelProcessor; /// /// Внутренняя реализация . /// 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()) { 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; } /// public NumberFormatPattern? GetNumberFormat() { var cell = GetCellElement(); if (cell?.StyleIndex?.Value is not uint styleIndex) return null; return _writer.GetNumberFormat(styleIndex); } /// public CellAlign GetCellAlign() { var cell = GetCellElement(); if (cell?.StyleIndex?.Value is not uint styleIndex) return default; return _writer.GetCellAlign(styleIndex); } /// public CellBorder GetCellBorder() { var cell = GetCellElement(); if (cell?.StyleIndex?.Value is not uint styleIndex) return default; return _writer.GetCellBorder(styleIndex); } /// public CellFill GetCellFill() { var cell = GetCellElement(); if (cell?.StyleIndex?.Value is not uint styleIndex) return default; return _writer.GetCellFill(styleIndex); } /// 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; } /// 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; } } /// 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; } } /// 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; } } /// 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 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; } /// 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()) { string text = run.GetFirstChild()?.Text ?? string.Empty; RunFormat? format = null; if (run.RunProperties != null) { // Преобразуем RunProperties в RunFormat (можно вынести в отдельный метод) format = RunFormat.FromRunProperties(run.RunProperties); } textObj.Run(text, format); } return textObj; } } /// 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()) 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().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()) { 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().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()) { var text = run.GetFirstChild()?.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(); 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; } }