diff --git a/QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs b/QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs
new file mode 100644
index 0000000..924c65d
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/CellAddressHelper.cs
@@ -0,0 +1,48 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+internal static class CellAddressHelper
+{
+ private const string letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ private static readonly uint[] _powers = [1, 26, 676];
+
+ public static uint ColumnLetterToIndex(string col)
+ {
+ uint index = 0;
+ foreach (char ch in col)
+ {
+ char c = char.ToUpperInvariant(ch);
+ if (c < 'A' || c > 'Z') return 0;
+ index = index * 26 + (uint)(c - 'A' + 1);
+ }
+ return index;
+ }
+
+ public static string ColumnIndexToLetter(uint col)
+ {
+ if (col == 0) return "";
+ uint n = col;
+ char[] buf = new char[3];
+ int idx = 0;
+ while (n > 0)
+ {
+ n--;
+ buf[idx++] = (char)('A' + (n % 26));
+ n /= 26;
+ }
+ return new string(buf, 0, idx);
+ }
+
+ public static bool TryParseCellReference(string reference, out uint row, out uint col)
+ {
+ row = 0; col = 0;
+ if (string.IsNullOrEmpty(reference)) return false;
+ int i = 0;
+ while (i < reference.Length && char.IsLetter(reference[i])) i++;
+ if (i == 0) return false;
+ string colPart = reference.Substring(0, i);
+ string rowPart = reference.Substring(i);
+ if (!uint.TryParse(rowPart, out row)) return false;
+ col = CellAddressHelper.ColumnLetterToIndex(colPart);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/CellAlign.cs b/QWERTYkez.ExcelProcessor/Editors/CellAlign.cs
new file mode 100644
index 0000000..0d2d7ba
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/CellAlign.cs
@@ -0,0 +1,223 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Определяет выравнивание содержимого ячейки: горизонтальное, вертикальное, перенос текста и уменьшение по размеру.
+/// Все свойства опциональны. Если свойство не задано, соответствующий аспект не изменяется.
+///
+public readonly struct CellAlign : IEquatable
+{
+ /// Горизонтальное выравнивание.
+ public CellAlignHorizontal? Horizontal { get; init; }
+
+ /// Вертикальное выравнивание.
+ public CellAlignVertical? Vertical { get; init; }
+
+ /// Переносить ли текст по словам (многострочный режим).
+ public bool? WrapText { get; init; }
+
+ /// Уменьшать размер шрифта, чтобы текст поместился в ячейку.
+ public bool? ShrinkToFit { get; init; }
+
+ /// Преобразует горизонтальное выравнивание в тип Open XML.
+ public bool TryGetExcelHorizontalAlignment(out HorizontalAlignmentValues value)
+ {
+ if (Horizontal.HasValue)
+ {
+ value = Horizontal.Value switch
+ {
+ CellAlignHorizontal.Left => HorizontalAlignmentValues.Left,
+ CellAlignHorizontal.Center => HorizontalAlignmentValues.Center,
+ CellAlignHorizontal.Right => HorizontalAlignmentValues.Right,
+ CellAlignHorizontal.Fill => HorizontalAlignmentValues.Fill,
+ CellAlignHorizontal.Justify => HorizontalAlignmentValues.Justify,
+ CellAlignHorizontal.CenterContinuous => HorizontalAlignmentValues.CenterContinuous,
+ CellAlignHorizontal.Distributed => HorizontalAlignmentValues.Distributed,
+ _ => throw new NotSupportedException($"Unsupported horizontal alignment: {Horizontal.Value}")
+ };
+ return true;
+ }
+ value = default;
+ return false;
+ }
+
+ /// Преобразует вертикальное выравнивание в тип Open XML.
+ public bool TryGetExcelVerticalAlignment(out VerticalAlignmentValues value)
+ {
+ if (Vertical.HasValue)
+ {
+ value = Vertical.Value switch
+ {
+ CellAlignVertical.Top => VerticalAlignmentValues.Top,
+ CellAlignVertical.Center => VerticalAlignmentValues.Center,
+ CellAlignVertical.Bottom => VerticalAlignmentValues.Bottom,
+ CellAlignVertical.Justify => VerticalAlignmentValues.Justify,
+ CellAlignVertical.Distributed => VerticalAlignmentValues.Distributed,
+ _ => throw new NotSupportedException($"Unsupported vertical alignment: {Vertical.Value}")
+ };
+ return true;
+ }
+ value = default;
+ return false;
+ }
+
+ /// Создаёт элемент Alignment для Open XML.
+ public Alignment? ToAlignment()
+ {
+ if (!Horizontal.HasValue && !Vertical.HasValue && !WrapText.HasValue && !ShrinkToFit.HasValue)
+ return null;
+
+ var align = new Alignment();
+ if (TryGetExcelHorizontalAlignment(out var hAlign))
+ align.Horizontal = hAlign;
+ if (TryGetExcelVerticalAlignment(out var vAlign))
+ align.Vertical = vAlign;
+ if (WrapText.HasValue)
+ align.WrapText = WrapText.Value;
+ if (ShrinkToFit.HasValue)
+ align.ShrinkToFit = ShrinkToFit.Value;
+ return align;
+ }
+
+ /// Создаёт CellAlign из элемента Alignment Open XML.
+ public static CellAlign FromAlignment(Alignment? alignment)
+ {
+ if (alignment == null)
+ return default;
+
+ var result = new CellAlign();
+ if (alignment.Horizontal?.Value is { } horizontal)
+ {
+ result = result with { Horizontal = MapHorizontalFromExcel(horizontal) };
+ }
+ if (alignment.Vertical?.Value is { } vertical)
+ {
+ result = result with { Vertical = MapVerticalFromExcel(vertical) };
+ }
+ if (alignment.WrapText?.Value is { } wrapText)
+ {
+ result = result with { WrapText = wrapText };
+ }
+ if (alignment.ShrinkToFit?.Value is { } shrinkToFit)
+ {
+ result = result with { ShrinkToFit = shrinkToFit };
+ }
+ return result;
+ }
+
+ private static CellAlignHorizontal MapHorizontalFromExcel(HorizontalAlignmentValues value)
+ {
+ if (value == HorizontalAlignmentValues.Left)
+ {
+ return CellAlignHorizontal.Left;
+ }
+ else if (value == HorizontalAlignmentValues.Center)
+ {
+ return CellAlignHorizontal.Center;
+ }
+ else if (value == HorizontalAlignmentValues.Right)
+ {
+ return CellAlignHorizontal.Right;
+ }
+ else if (value == HorizontalAlignmentValues.Fill)
+ {
+ return CellAlignHorizontal.Fill;
+ }
+ else if (value == HorizontalAlignmentValues.Justify)
+ {
+ return CellAlignHorizontal.Justify;
+ }
+ else if (value == HorizontalAlignmentValues.CenterContinuous)
+ {
+ return CellAlignHorizontal.CenterContinuous;
+ }
+ else if (value == HorizontalAlignmentValues.Distributed)
+ {
+ return CellAlignHorizontal.Distributed;
+ }
+ else throw new NotSupportedException($"Unsupported horizontal alignment: {value}");
+ }
+
+ private static CellAlignVertical MapVerticalFromExcel(VerticalAlignmentValues value)
+ {
+ if (value == VerticalAlignmentValues.Top)
+ {
+ return CellAlignVertical.Top;
+ }
+ else if (value == VerticalAlignmentValues.Center)
+ {
+ return CellAlignVertical.Center;
+ }
+ else if (value == VerticalAlignmentValues.Bottom)
+ {
+ return CellAlignVertical.Bottom;
+ }
+ else if (value == VerticalAlignmentValues.Justify)
+ {
+ return CellAlignVertical.Justify;
+ }
+ else if (value == VerticalAlignmentValues.Distributed)
+ {
+ return CellAlignVertical.Distributed;
+ }
+ else throw new NotSupportedException($"Unsupported vertical alignment: {value}");
+ }
+
+ public override bool Equals(object? obj) => obj is CellAlign other && Equals(other);
+ public bool Equals(CellAlign other) => this == other;
+
+ public static bool operator ==(CellAlign left, CellAlign right) =>
+ left.Horizontal == right.Horizontal &&
+ left.Vertical == right.Vertical &&
+ left.WrapText == right.WrapText &&
+ left.ShrinkToFit == right.ShrinkToFit;
+
+ public static bool operator !=(CellAlign left, CellAlign right) => !(left == right);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 31 + (Horizontal?.GetHashCode() ?? 0);
+ hash = hash * 31 + (Vertical?.GetHashCode() ?? 0);
+ hash = hash * 31 + (WrapText?.GetHashCode() ?? 0);
+ hash = hash * 31 + (ShrinkToFit?.GetHashCode() ?? 0);
+ return hash;
+ }
+ }
+}
+
+
+/// Горизонтальное выравнивание содержимого ячейки.
+public enum CellAlignHorizontal
+{
+ /// По левому краю.
+ Left,
+ /// По центру.
+ Center,
+ /// По правому краю.
+ Right,
+ /// Заполнение (повтор содержимого для заполнения ширины).
+ Fill,
+ /// По ширине (для многострочного текста).
+ Justify,
+ /// Центрирование по выделенным ячейкам (визуально, без объединения).
+ CenterContinuous,
+ /// Распределённый (выравнивание по ширине с пробелами).
+ Distributed,
+}
+
+/// Вертикальное выравнивание содержимого ячейки.
+public enum CellAlignVertical
+{
+ /// По верхнему краю.
+ Top,
+ /// По центру.
+ Center,
+ /// По нижнему краю (значение по умолчанию).
+ Bottom,
+ /// По высоте (для многострочного текста).
+ Justify,
+ /// Распределённый по вертикали.
+ Distributed,
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/CellBorder.cs b/QWERTYkez.ExcelProcessor/Editors/CellBorder.cs
new file mode 100644
index 0000000..857ba67
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/CellBorder.cs
@@ -0,0 +1,281 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Определяет границы ячейки: верхнюю, нижнюю, левую, правую и диагональные.
+/// Каждая граница может иметь стиль и цвет.
+///
+public readonly struct CellBorder : IEquatable
+{
+ /// Верхняя граница.
+ public BorderSide? TopBorder { get; init; }
+
+ /// Нижняя граница.
+ public BorderSide? BottomBorder { get; init; }
+
+ /// Левая граница.
+ public BorderSide? LeftBorder { get; init; }
+
+ /// Правая граница.
+ public BorderSide? RightBorder { get; init; }
+
+ /// Диагональная граница «из левого верхнего в правый нижний» (\\).
+ public BorderSide? DiagonalLeft { get; init; }
+
+ /// Диагональная граница «из левого нижнего в правый верхний» (//).
+ public BorderSide? DiagonalRight { get; init; }
+
+ /// Создаёт элемент Border для Open XML.
+ public Border? ToBorder()
+ {
+ bool hasAny = TopBorder.HasValue || BottomBorder.HasValue || LeftBorder.HasValue ||
+ RightBorder.HasValue || DiagonalLeft.HasValue || DiagonalRight.HasValue;
+ if (!hasAny) return null;
+
+ var border = new Border();
+ if (TopBorder.HasValue)
+ border.TopBorder = TopBorder.Value.ToBorderElement();
+ if (BottomBorder.HasValue)
+ border.BottomBorder = BottomBorder.Value.ToBorderElement();
+ if (LeftBorder.HasValue)
+ border.LeftBorder = LeftBorder.Value.ToBorderElement();
+ if (RightBorder.HasValue)
+ border.RightBorder = RightBorder.Value.ToBorderElement();
+
+ // Обработка диагональных границ
+ if (DiagonalLeft.HasValue || DiagonalRight.HasValue)
+ {
+ // Диагональ \\ (из левого верхнего в правый нижний) = DiagonalDown
+ border.DiagonalDown = DiagonalLeft.HasValue;
+ // Диагональ // (из левого нижнего в правый верхний) = DiagonalUp
+ border.DiagonalUp = DiagonalRight.HasValue;
+
+ // Стиль и цвет берём из первой заданной диагонали (например, DiagonalLeft)
+ var diagSide = (DiagonalLeft ?? DiagonalRight)!.Value;
+ border.DiagonalBorder = diagSide.ToBorderElement();
+ }
+
+ return border;
+ }
+
+ /// Создаёт CellBorder из элемента Border Open XML.
+ public static CellBorder FromBorder(Border? border)
+ {
+ if (border == null)
+ return default;
+
+ var result = new CellBorder
+ {
+ TopBorder = BorderSide.FromBorderProperties(border.TopBorder),
+ BottomBorder = BorderSide.FromBorderProperties(border.BottomBorder),
+ LeftBorder = BorderSide.FromBorderProperties(border.LeftBorder),
+ RightBorder = BorderSide.FromBorderProperties(border.RightBorder)
+ };
+
+ // Диагональные границы
+ if (border.DiagonalDown?.Value == true)
+ result = result with { DiagonalLeft = BorderSide.FromBorderProperties(border.DiagonalBorder) };
+ if (border.DiagonalUp?.Value == true)
+ result = result with { DiagonalRight = BorderSide.FromBorderProperties(border.DiagonalBorder) };
+
+ return result;
+ }
+
+ public override bool Equals(object? obj) => obj is CellBorder other && Equals(other);
+ public bool Equals(CellBorder other) => this == other;
+
+ public static bool operator ==(CellBorder left, CellBorder right) =>
+ Equals(left.TopBorder, right.TopBorder) &&
+ Equals(left.BottomBorder, right.BottomBorder) &&
+ Equals(left.LeftBorder, right.LeftBorder) &&
+ Equals(left.RightBorder, right.RightBorder) &&
+ Equals(left.DiagonalLeft, right.DiagonalLeft) &&
+ Equals(left.DiagonalRight, right.DiagonalRight);
+
+ public static bool operator !=(CellBorder left, CellBorder right) => !(left == right);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 31 + (TopBorder?.GetHashCode() ?? 0);
+ hash = hash * 31 + (BottomBorder?.GetHashCode() ?? 0);
+ hash = hash * 31 + (LeftBorder?.GetHashCode() ?? 0);
+ hash = hash * 31 + (RightBorder?.GetHashCode() ?? 0);
+ hash = hash * 31 + (DiagonalLeft?.GetHashCode() ?? 0);
+ hash = hash * 31 + (DiagonalRight?.GetHashCode() ?? 0);
+ return hash;
+ }
+ }
+}
+
+/// Стиль и цвет границы.
+public readonly struct BorderSide : IEquatable
+{
+ /// Стиль линии границы.
+ public BorderStyle? Style { get; init; }
+
+ /// Цвет границы.
+ public ExColor? Color { get; init; }
+
+ internal T ToBorderElement() where T : BorderPropertiesType, new()
+ {
+ var element = new T();
+ if (Style.HasValue)
+ {
+ element.Style = Style.Value switch
+ {
+ BorderStyle.Thin => BorderStyleValues.Thin,
+ BorderStyle.Medium => BorderStyleValues.Medium,
+ BorderStyle.Dashed => BorderStyleValues.Dashed,
+ BorderStyle.Dotted => BorderStyleValues.Dotted,
+ BorderStyle.Thick => BorderStyleValues.Thick,
+ BorderStyle.Double => BorderStyleValues.Double,
+ BorderStyle.Hair => BorderStyleValues.Hair,
+ BorderStyle.MediumDashed => BorderStyleValues.MediumDashed,
+ BorderStyle.DashDot => BorderStyleValues.DashDot,
+ BorderStyle.MediumDashDot => BorderStyleValues.MediumDashDot,
+ BorderStyle.DashDotDot => BorderStyleValues.DashDotDot,
+ BorderStyle.MediumDashDotDot => BorderStyleValues.MediumDashDotDot,
+ BorderStyle.SlantDashDot => BorderStyleValues.SlantDashDot,
+ _ => throw new NotImplementedException(),
+ };
+ }
+ if (Color.HasValue && Color.Value.Color.HasValue)
+ {
+ var c = Color.Value.Color.Value;
+ element.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" };
+ }
+ return element;
+ }
+
+ /// Создаёт BorderSide из элемента границы Open XML (TopBorder, BottomBorder, LeftBorder, RightBorder, DiagonalBorder).
+ public static BorderSide FromBorderProperties(BorderPropertiesType? borderElement)
+ {
+ if (borderElement is null)
+ return default;
+
+ var result = new BorderSide();
+ if (borderElement.Style is not null)
+ {
+ result = result with { Style = MapBorderStyleFromExcel(borderElement.Style.Value) };
+ }
+ if (borderElement.Color?.Rgb?.Value is { } rgb && rgb.Length >= 6)
+ {
+ var color = System.Drawing.Color.FromArgb(
+ Convert.ToByte(rgb.Substring(0, 2), 16),
+ Convert.ToByte(rgb.Substring(2, 2), 16),
+ Convert.ToByte(rgb.Substring(4, 2), 16)
+ );
+ result = result with { Color = new ExColor(color) };
+ }
+ return result;
+ }
+
+ private static BorderStyle MapBorderStyleFromExcel(BorderStyleValues value)
+ {
+ if (value == BorderStyleValues.Thin)
+ {
+ return BorderStyle.Thin;
+ }
+ else if (value == BorderStyleValues.Medium)
+ {
+ return BorderStyle.Medium;
+ }
+ else if (value == BorderStyleValues.Dashed)
+ {
+ return BorderStyle.Dashed;
+ }
+ else if (value == BorderStyleValues.Dotted)
+ {
+ return BorderStyle.Dotted;
+ }
+ else if (value == BorderStyleValues.Thick)
+ {
+ return BorderStyle.Thick;
+ }
+ else if (value == BorderStyleValues.Double)
+ {
+ return BorderStyle.Double;
+ }
+ else if (value == BorderStyleValues.Hair)
+ {
+ return BorderStyle.Hair;
+ }
+ else if (value == BorderStyleValues.MediumDashed)
+ {
+ return BorderStyle.MediumDashed;
+ }
+ else if (value == BorderStyleValues.DashDot)
+ {
+ return BorderStyle.DashDot;
+ }
+ else if (value == BorderStyleValues.MediumDashDot)
+ {
+ return BorderStyle.MediumDashDot;
+ }
+ else if (value == BorderStyleValues.DashDotDot)
+ {
+ return BorderStyle.DashDotDot;
+ }
+ else if (value == BorderStyleValues.MediumDashDotDot)
+ {
+ return BorderStyle.MediumDashDotDot;
+ }
+ else if (value == BorderStyleValues.SlantDashDot)
+ {
+ return BorderStyle.SlantDashDot;
+ }
+ else throw new NotSupportedException($"Unsupported border style: {value}");
+ }
+
+ public override bool Equals(object? obj) => obj is BorderSide other && Equals(other);
+ public bool Equals(BorderSide other) => this == other;
+
+ public static bool operator ==(BorderSide left, BorderSide right) =>
+ left.Style == right.Style && Equals(left.Color, right.Color);
+
+ public static bool operator !=(BorderSide left, BorderSide right) => !(left == right);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 31 + Style.GetHashCode();
+ hash = hash * 31 + (Color?.GetHashCode() ?? 0);
+ return hash;
+ }
+ }
+}
+
+/// Тип линии границы
+public enum BorderStyle
+{
+ /// Тонкая линия
+ Thin,
+ /// Средняя линия
+ Medium,
+ /// Штриховая
+ Dashed,
+ /// Точечная
+ Dotted,
+ /// Толстая линия
+ Thick,
+ /// Двойная линия
+ Double,
+ /// Волосяная (очень тонкая)
+ Hair,
+ /// Средняя штриховая
+ MediumDashed,
+ /// Штрих-пунктирная
+ DashDot,
+ /// Средняя штрих-пунктирная
+ MediumDashDot,
+ /// Штрих-пунктир-пунктир
+ DashDotDot,
+ /// Средняя штрих-пунктир-пунктир
+ MediumDashDotDot,
+ /// Наклонная штрих-пунктирная (для диагональных)
+ SlantDashDot,
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/CellFill.cs b/QWERTYkez.ExcelProcessor/Editors/CellFill.cs
new file mode 100644
index 0000000..c2a978f
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/CellFill.cs
@@ -0,0 +1,53 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Определяет заливку (фон) ячейки.
+///
+public readonly struct CellFill : IEquatable
+{
+ /// Цвет фона.
+ public ExColor? BackgroundColor { get; init; }
+
+ /// Создаёт элемент Fill для Open XML.
+ public Fill? ToFill()
+ {
+ if (!BackgroundColor.HasValue || !BackgroundColor.Value.Color.HasValue)
+ return null;
+
+ var c = BackgroundColor.Value.Color.Value;
+ var fill = new Fill
+ {
+ PatternFill = new PatternFill
+ {
+ PatternType = PatternValues.Solid,
+ ForegroundColor = new ForegroundColor { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" }
+ }
+ };
+ return fill;
+ }
+
+ /// Создаёт CellFill из элемента Fill Open XML.
+ public static CellFill FromFill(Fill? fill)
+ {
+ if (fill?.PatternFill?.ForegroundColor?.Rgb?.Value is not string rgb || rgb.Length < 6)
+ return default;
+
+ var color = System.Drawing.Color.FromArgb(
+ Convert.ToByte(rgb.Substring(0, 2), 16),
+ Convert.ToByte(rgb.Substring(2, 2), 16),
+ Convert.ToByte(rgb.Substring(4, 2), 16)
+ );
+ return new CellFill { BackgroundColor = new ExColor(color) };
+ }
+
+ public override bool Equals(object? obj) => obj is CellFill other && Equals(other);
+ public bool Equals(CellFill other) => this == other;
+
+ public static bool operator ==(CellFill left, CellFill right) =>
+ Equals(left.BackgroundColor, right.BackgroundColor);
+
+ public static bool operator !=(CellFill left, CellFill right) => !(left == right);
+
+ public override int GetHashCode() =>
+ BackgroundColor?.GetHashCode() ?? 0;
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/CellFont.cs b/QWERTYkez.ExcelProcessor/Editors/CellFont.cs
new file mode 100644
index 0000000..48dab60
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/CellFont.cs
@@ -0,0 +1,112 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Определяет шрифтовое оформление всей ячейки: размер, семейство, цвет, начертание.
+/// Все свойства опциональны.
+///
+public readonly struct CellFont : IEquatable
+{
+ /// Размер шрифта в пунктах (например, 11).
+ public double? FontSize { get; init; }
+
+ /// Имя семейства шрифта (например, "Calibri", "Arial").
+ public string? FontFamily { get; init; }
+
+ /// Цвет текста.
+ public ExColor? FontColor { get; init; }
+
+ /// Жирное начертание.
+ public bool? IsBold { get; init; }
+
+ /// Курсив.
+ public bool? IsItalic { get; init; }
+
+ /// Подчёркивание (одинарное).
+ public bool? IsUnderline { get; init; }
+
+ /// Зачёркивание.
+ public bool? IsStrike { get; init; }
+
+ /// Создаёт элемент Font для Open XML.
+ public Font? ToFont()
+ {
+ if (!FontSize.HasValue && FontFamily is null && FontColor is null &&
+ !IsBold.HasValue && !IsItalic.HasValue && !IsUnderline.HasValue && !IsStrike.HasValue)
+ return null;
+
+ var font = new Font();
+ if (FontSize.HasValue)
+ font.FontSize = new FontSize { Val = FontSize.Value };
+ if (FontFamily is not null)
+ font.FontName = new FontName { Val = FontFamily };
+ if (FontColor.HasValue && FontColor.Value.Color.HasValue)
+ {
+ var c = FontColor.Value.Color.Value;
+ font.Color = new Color { Rgb = $"{c.R:X2}{c.G:X2}{c.B:X2}" };
+ }
+ if (IsBold == true) font.Bold = new Bold();
+ if (IsItalic == true) font.Italic = new Italic();
+ if (IsUnderline == true) font.Underline = new Underline();
+ if (IsStrike == true) font.Strike = new Strike();
+ return font;
+ }
+
+ /// Создаёт CellFont из элемента Font Open XML.
+ public static CellFont FromFont(Font? font)
+ {
+ if (font == null)
+ return default;
+
+ var result = new CellFont
+ {
+ IsBold = font.Bold != null,
+ IsItalic = font.Italic != null,
+ IsUnderline = font.Underline != null,
+ IsStrike = font.Strike != null
+ };
+ if (font.FontSize?.Val?.Value is { } size)
+ result = result with { FontSize = size };
+ if (font.FontName?.Val?.Value is { } name)
+ result = result with { FontFamily = name };
+ if (font.Color?.Rgb?.Value is { } rgb && rgb.Length >= 6)
+ {
+ var color = System.Drawing.Color.FromArgb(
+ Convert.ToByte(rgb.Substring(0, 2), 16),
+ Convert.ToByte(rgb.Substring(2, 2), 16),
+ Convert.ToByte(rgb.Substring(4, 2), 16)
+ );
+ result = result with { FontColor = new ExColor(color) };
+ }
+ return result;
+ }
+
+ public override bool Equals(object? obj) => obj is CellFont other && Equals(other);
+ public bool Equals(CellFont other) => this == other;
+
+ public static bool operator ==(CellFont left, CellFont right) =>
+ left.FontSize == right.FontSize &&
+ left.FontFamily == right.FontFamily &&
+ Equals(left.FontColor, right.FontColor) &&
+ left.IsBold == right.IsBold &&
+ left.IsItalic == right.IsItalic &&
+ left.IsUnderline == right.IsUnderline &&
+ left.IsStrike == right.IsStrike;
+
+ public static bool operator !=(CellFont left, CellFont right) => !(left == right);
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ int hash = 17;
+ hash = hash * 31 + (FontSize?.GetHashCode() ?? 0);
+ hash = hash * 31 + (FontFamily?.GetHashCode() ?? 0);
+ hash = hash * 31 + (FontColor?.GetHashCode() ?? 0);
+ hash = hash * 31 + (IsBold?.GetHashCode() ?? 0);
+ hash = hash * 31 + (IsItalic?.GetHashCode() ?? 0);
+ hash = hash * 31 + (IsUnderline?.GetHashCode() ?? 0);
+ hash = hash * 31 + (IsStrike?.GetHashCode() ?? 0);
+ return hash;
+ }
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs b/QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs
new file mode 100644
index 0000000..a0a7fc8
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ColumnWidth.cs
@@ -0,0 +1,63 @@
+namespace QWERTYkez.ExcelProcessor.Editors
+{
+ ///
+ /// Представляет ширину столбца в Excel. Поддерживает задание в символах, пунктах, сантиметрах и миллиметрах.
+ /// Для методов FromCM/FromMM/FromPoints требуется калибровка через , иначе точность не гарантирована.
+ ///
+ public readonly struct ColumnWidth
+ {
+ private readonly double _rawValue;
+ private readonly UnitType _unit;
+
+ private enum UnitType { Characters, Points, Centimeters, Millimeters }
+
+ /// Коэффициент перевода символов в пункты по умолчанию (используется, если нет калибровочной таблицы).
+ public static double DefaultPointsPerChar { get; set; } = 5.65;
+
+ private ColumnWidth(double value, UnitType unit)
+ {
+ _rawValue = value;
+ _unit = unit;
+ }
+
+ /// Задаёт ширину в character units (символах стандартного шрифта).
+ public static ColumnWidth FromCharacters(double characters) => new(characters, UnitType.Characters);
+
+ /// Задаёт ширину в пунктах.
+ [Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
+ public static ColumnWidth FromPoints(double points) => new(points, UnitType.Points);
+
+ /// Задаёт ширину в сантиметрах.
+ [Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
+ public static ColumnWidth FromCM(double cm) => new(cm, UnitType.Centimeters);
+
+ /// Задаёт ширину в миллиметрах.
+ [Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
+ public static ColumnWidth FromMM(double mm) => new(mm, UnitType.Millimeters);
+
+ /// Возвращает целевую ширину в пунктах (если ширина задана в пунктах, см или мм).
+ internal double GetTargetPoints()
+ {
+ return _unit switch
+ {
+ UnitType.Points => _rawValue,
+ UnitType.Centimeters => _rawValue * 28.3464566929,
+ UnitType.Millimeters => (_rawValue / 10.0) * 28.3464566929,
+ _ => double.NaN
+ };
+ }
+ internal double GetTargetCentimeters()
+ {
+ return _unit switch
+ {
+ UnitType.Centimeters => _rawValue,
+ UnitType.Millimeters => _rawValue / 10.0,
+ UnitType.Points => _rawValue / 28.3464566929,
+ _ => double.NaN
+ };
+ }
+
+ internal bool IsCharacterUnit => _unit == UnitType.Characters;
+ internal double CharacterValue => IsCharacterUnit ? _rawValue : double.NaN;
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExColor.cs b/QWERTYkez.ExcelProcessor/Editors/ExColor.cs
new file mode 100644
index 0000000..0835a65
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExColor.cs
@@ -0,0 +1,62 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+public readonly struct ExColor(System.Drawing.Color? Color)
+{
+ private readonly System.Drawing.Color? color = Color;
+
+ /// Проверяет, является ли цвет автоматическим (т.е. Color == null).
+ public bool IsAuto => color is null;
+
+ public System.Drawing.Color? Color => color;
+
+ public static ExColor FromArgb(byte r, byte g, byte b) => new(System.Drawing.Color.FromArgb(r, g, b));
+ public static ExColor FromName(string knownColor) => new(System.Drawing.Color.FromName(knownColor));
+
+
+
+ public static implicit operator ExColor(Color exColor) => FromExcel(exColor);
+ public static implicit operator Color(ExColor color) => color.ToExcel();
+ public static ExColor FromExcel(Color excelColor)
+ {
+ if (excelColor == null)
+ return new ExColor(null);
+
+ // Если цвет автоматический
+ if (excelColor.Auto != null && excelColor.Auto.Value)
+ return new ExColor(null);
+
+ // Если задан RGB
+ if (excelColor.Rgb?.Value is { } rgb && rgb.Length > 0)
+ {
+ if (rgb.Length == 6) // RRGGBB
+ {
+ byte r = Convert.ToByte(rgb.Substring(0, 2), 16);
+ byte g = Convert.ToByte(rgb.Substring(2, 2), 16);
+ byte b = Convert.ToByte(rgb.Substring(4, 2), 16);
+ return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
+ }
+ else if (rgb.Length == 8) // AARRGGBB (альфа игнорируется)
+ {
+ byte r = Convert.ToByte(rgb.Substring(2, 2), 16);
+ byte g = Convert.ToByte(rgb.Substring(4, 2), 16);
+ byte b = Convert.ToByte(rgb.Substring(6, 2), 16);
+ return new ExColor(System.Drawing.Color.FromArgb(r, g, b));
+ }
+ }
+
+ // По умолчанию — автоматический цвет
+ return new ExColor(null);
+ }
+ public Color ToExcel()
+ {
+ var excelColor = new Color();
+ {
+ if (color.HasValue)
+ {
+ excelColor.Rgb = $"{color.Value.R:X2}{color.Value.G:X2}{color.Value.B:X2}";
+ }
+ else excelColor.Auto = true;
+ }
+ return excelColor;
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs
new file mode 100644
index 0000000..4d032e1
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs
@@ -0,0 +1,43 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Внутренняя реализация , которая делегирует вызовы к .
+/// Используется в ситуациях, когда требуется отдельный объект книги (например, в ).
+///
+internal sealed class ExcelBook : IBook
+{
+ internal readonly ExcelWriter Writer;
+
+ ///
+ /// Инициализирует обёртку книги.
+ ///
+ /// Реальный процессор Excel (реализует IBook).
+ internal ExcelBook(ExcelWriter writer)
+ {
+ Writer = writer;
+ }
+
+ ///
+ public IReadOnlyList GetSheets() => Writer.GetSheets();
+
+ ///
+ public ISheet? Sheet(string name) => Writer.Sheet(name);
+
+ ///
+ public bool TryGetSheet(string name, out ISheet sheet) => Writer.TryGetSheet(name, out sheet);
+
+ ///
+ public bool TryAddSheet(string name, Action? edit = null) => Writer.TryAddSheet(name, edit);
+
+ ///
+ public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name);
+
+ ///
+ public bool TryRemoveSheet(ISheet sheet) => Writer.TryRemoveSheet(sheet);
+
+ ///
+ public IReadOnlyList GetNumberFormats() => Writer.GetNumberFormats();
+
+ ///
+ public NumberFormatPattern CreateNumberFormat(string format) => Writer.CreateNumberFormat(format);
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs
new file mode 100644
index 0000000..a474389
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExcelCell.cs
@@ -0,0 +1,742 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Внутренняя реализация .
+///
+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 Set(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 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;
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelCellText.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelCellText.cs
new file mode 100644
index 0000000..a80f2b3
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExcelCellText.cs
@@ -0,0 +1,270 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Внутренняя реализация для работы с богатым текстом ячейки.
+/// Хранит коллекцию фрагментов .
+/// Минимизирует аллокации, не использует рефлексию.
+///
+internal sealed class ExcelCellText : ICellText
+{
+ private List? _runs;
+
+ ///
+ public int Count => _runs?.Count ?? 0;
+
+ ///
+ public IEnumerable GetRuns()
+ {
+ if (_runs is null)
+ return [];
+ // Возвращаем сам список, чтобы избежать копирования.
+ // Вызывающий не должен модифицировать коллекцию.
+ return _runs;
+ }
+
+ ///
+ public IRun? GetRunAt(int index)
+ {
+ if (_runs is null || index < 0 || index >= _runs.Count)
+ return null;
+ return _runs[index];
+ }
+
+ ///
+ public bool TryGetRunAt(int index, out IRun run)
+ {
+ run = GetRunAt(index)!;
+ return run != null;
+ }
+
+ ///
+ public IRun? First()
+ {
+ if (_runs is null || _runs.Count == 0)
+ return null;
+ return _runs[0];
+ }
+
+ ///
+ public bool TryGetFirst(out IRun run)
+ {
+ run = First()!;
+ return run != null;
+ }
+
+ ///
+ public IRun? Last()
+ {
+ if (_runs is null || _runs.Count == 0)
+ return null;
+ return _runs[_runs.Count - 1];
+ }
+
+ ///
+ public bool TryGetLast(out IRun run)
+ {
+ run = Last()!;
+ return run != null;
+ }
+
+ ///
+ public bool TryRemoveRun(IRun run)
+ {
+ if (run is null || _runs is null)
+ return false;
+ return _runs.Remove(run);
+ }
+
+ ///
+ public bool TryRemoveRun(int index)
+ {
+ if (_runs is null || index < 0 || index >= _runs.Count)
+ return false;
+ _runs.RemoveAt(index);
+ return true;
+ }
+
+ ///
+ public bool TryRemoveRun(int index, out IRun? removed)
+ {
+ removed = GetRunAt(index);
+ if (removed is null)
+ return false;
+ return TryRemoveRun(index);
+ }
+
+ ///
+ public ICellText AddBreak()
+ {
+ // Добавляем символ переноса строки в последний существующий Run
+ if (_runs != null && _runs.Count > 0)
+ {
+ var lastRun = _runs[_runs.Count - 1];
+ lastRun.Text += "\n";
+ }
+ else
+ {
+ // Если нет ни одного Run, создаём новый с символом переноса
+ AddRun("\n", null);
+ }
+ return this;
+ }
+
+ ///
+ public ICellText AddRun(string text, RunFormat? format = null)
+ {
+ if (string.IsNullOrEmpty(text))
+ return this;
+ _runs ??= [];
+ var run = new ExcelRun { Text = text, Format = format };
+ _runs.Add(run);
+ return this;
+ }
+
+ ///
+ public ICellText AddRunBreak(string text, RunFormat? format = null)
+ {
+ AddRun(text, format);
+ AddBreak();
+ return this;
+ }
+
+ ///
+ public ICellText AddSubRun(string text, RunFormat? format = null)
+ {
+ var subFormat = format is { } fmt
+ ? new RunFormat
+ {
+ IsBold = fmt.IsBold,
+ IsItalic = fmt.IsItalic,
+ Underline = fmt.Underline,
+ IsStrike = fmt.IsStrike,
+ Color = fmt.Color,
+ FontSize = fmt.FontSize,
+ FontFamily = fmt.FontFamily,
+ Vertical = VerticalTextRunAlignment.Subscript
+ }
+ : new RunFormat { Vertical = VerticalTextRunAlignment.Subscript };
+ return AddRun(text, subFormat);
+ }
+
+ ///
+ public ICellText AddSupRun(string text, RunFormat? format = null)
+ {
+ var supFormat = format is { } fmt
+ ? new RunFormat
+ {
+ IsBold = fmt.IsBold,
+ IsItalic = fmt.IsItalic,
+ Underline = fmt.Underline,
+ IsStrike = fmt.IsStrike,
+ Color = fmt.Color,
+ FontSize = fmt.FontSize,
+ FontFamily = fmt.FontFamily,
+ Vertical = VerticalTextRunAlignment.Subscript
+ }
+ : new RunFormat { Vertical = VerticalTextRunAlignment.Superscript };
+ return AddRun(text, supFormat);
+ }
+
+ ///
+ public bool TryInsertRun(int index, string text, RunFormat? format = null)
+ {
+ if (index < 0 || string.IsNullOrEmpty(text))
+ return false;
+ _runs ??= [];
+ if (index > _runs.Count)
+ return false;
+ var run = new ExcelRun { Text = text, Format = format };
+ _runs.Insert(index, run);
+ return true;
+ }
+
+ ///
+ public bool TryInsertRunBreak(int index, string text, RunFormat? format = null)
+ {
+ if (!TryInsertRun(index, text, format))
+ return false;
+ // После вставленного run добавляем break на следующей позиции
+ AddBreak();
+ // Сдвигаем? Просто добавляем break в конец – неверно. Break должен быть сразу после вставленного.
+ // Но AddBreak добавляет в конец. Нужно вставить break на index+1.
+ return TryInsertRun(index + 1, "\n", null);
+ }
+
+ ///
+ public bool TryInsertSubRun(int index, string text, RunFormat? format = null)
+ {
+ var subFormat = format is { } fmt
+ ? new RunFormat
+ {
+ IsBold = fmt.IsBold,
+ IsItalic = fmt.IsItalic,
+ Underline = fmt.Underline,
+ IsStrike = fmt.IsStrike,
+ Color = fmt.Color,
+ FontSize = fmt.FontSize,
+ FontFamily = fmt.FontFamily,
+ Vertical = VerticalTextRunAlignment.Subscript
+ }
+ : new RunFormat { Vertical = VerticalTextRunAlignment.Subscript };
+ return TryInsertRun(index, text, subFormat);
+ }
+
+ ///
+ public bool TryInsertSupRun(int index, string text, RunFormat? format = null)
+ {
+ var supFormat = format is { } fmt
+ ? new RunFormat
+ {
+ IsBold = fmt.IsBold,
+ IsItalic = fmt.IsItalic,
+ Underline = fmt.Underline,
+ IsStrike = fmt.IsStrike,
+ Color = fmt.Color,
+ FontSize = fmt.FontSize,
+ FontFamily = fmt.FontFamily,
+ Vertical = VerticalTextRunAlignment.Subscript
+ }
+ : new RunFormat { Vertical = VerticalTextRunAlignment.Superscript };
+ return TryInsertRun(index, text, supFormat);
+ }
+
+ ///
+ public void ApplyFormatToAllRuns(RunFormat format)
+ {
+ if (_runs is null || _runs.Count == 0)
+ return;
+ foreach (var run in _runs)
+ {
+ if (run is ExcelRun xRun)
+ {
+ // Объединение форматов: ненулевые свойства overlay заменяют значения в base.
+ var baseFmt = xRun.Format ?? new RunFormat();
+ xRun.Format = MergeRunFormat(baseFmt, format);
+ }
+ }
+ }
+
+ ///
+ public void Clear()
+ {
+ _runs?.Clear();
+ _runs = null;
+ }
+
+ private static RunFormat MergeRunFormat(RunFormat baseFmt, RunFormat overlay)
+ {
+ return new RunFormat
+ {
+ IsBold = overlay.IsBold ?? baseFmt.IsBold,
+ IsItalic = overlay.IsItalic ?? baseFmt.IsItalic,
+ Underline = overlay.Underline ?? baseFmt.Underline,
+ IsStrike = overlay.IsStrike ?? baseFmt.IsStrike,
+ Color = overlay.Color ?? baseFmt.Color,
+ FontSize = overlay.FontSize ?? baseFmt.FontSize,
+ FontFamily = overlay.FontFamily ?? baseFmt.FontFamily,
+ Vertical = overlay.Vertical ?? baseFmt.Vertical
+ };
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs
new file mode 100644
index 0000000..5ccbfc2
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExcelColumn.cs
@@ -0,0 +1,502 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Внутренняя реализация .
+///
+internal sealed class ExcelColumn : IColumn
+{
+ internal readonly ExcelWriter _writer;
+ internal readonly ExcelSheet _sheet;
+ internal uint _colIndex;
+
+ internal ExcelColumn(ExcelWriter writer, ExcelSheet sheet, uint colIndex)
+ {
+ _writer = writer;
+ _sheet = sheet;
+ _colIndex = colIndex;
+ }
+
+ public uint Index => _colIndex;
+ public string IndexLetter => NumberToColumnLetter(_colIndex);
+
+
+
+ /// Устанавливает ширину столбца без создания объекта IColumn.
+ internal static void SetWidth(ExcelWriter writer, ExcelSheet sheet, uint colIndex, ColumnWidth width)
+ {
+ writer.ThrowIfDisposed();
+ lock (writer._syncLock)
+ {
+ var col = GetOrCreateColumnElementInternal(sheet, colIndex);
+
+ double widthInChars;
+
+ if (width.IsCharacterUnit)
+ {
+ // Если ширина задана в символах, используем напрямую
+ widthInChars = width.CharacterValue;
+ }
+ else
+ {
+ double targetPoints = width.GetTargetPoints();
+ widthInChars = writer.TryGetCalibrateCoeff(targetPoints, out var closestCw)
+ ? closestCw : targetPoints / ColumnWidth.DefaultPointsPerChar;
+ }
+
+
+ col.Width = widthInChars;
+ col.CustomWidth = true;
+ }
+ }
+
+ /// Возвращает ширину столбца.
+ internal static ColumnWidth GetWidth(ExcelWriter writer, ExcelSheet sheet, uint colIndex)
+ {
+ writer.ThrowIfDisposed();
+ lock (writer._syncLock)
+ {
+ var col = GetColumnElementInternal(sheet, colIndex);
+ if (col is Column column
+ && column.CustomWidth?.Value == true
+ && column.Width?.Value is { } wid)
+ return ColumnWidth.FromCharacters(wid);
+ return ColumnWidth.FromCharacters(8.43); // стандартная ширина
+ }
+ }
+
+ // Вспомогательные внутренние методы (перенести существующую логику)
+ private static Column? GetColumnElementInternal(ExcelSheet sheet, uint colIndex)
+ {
+ var cols = sheet.Worksheet.GetFirstChild();
+ if (cols == null) return null;
+ foreach (Column col in cols.Elements())
+ {
+ if (col?.Min?.Value is { } min
+ && col?.Max?.Value is { } max
+ && min <= colIndex
+ && max >= colIndex)
+ return col;
+ }
+ return null;
+ }
+
+ private static Column GetOrCreateColumnElementInternal(ExcelSheet sheet, uint colIndex)
+ {
+ var existing = GetColumnElementInternal(sheet, colIndex);
+ 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 = colIndex,
+ Max = colIndex,
+ Width = 8.43,
+ CustomWidth = true
+ };
+ cols.Append(newCol);
+ return newCol;
+ }
+
+
+
+ public IColumn MoveTo(uint index)
+ {
+ if (index == _colIndex) return this;
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ // Копируем данные в новую позицию
+ CopyColumnData(_colIndex, index);
+ // Очищаем исходный столбец
+ ClearColumnData(_colIndex);
+ _colIndex = index;
+ }
+ return this;
+ }
+
+ public IColumn MoveTo(string index)
+ {
+ uint idx = CellAddressHelper.ColumnLetterToIndex(index);
+ return MoveTo(idx);
+ }
+
+ public ColumnWidth Width
+{
+ get
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var column = GetColumnElement();
+ if (column is Column col && col.Width?.Value is { } val && col.CustomWidth?.Value == true)
+ return ColumnWidth.FromCharacters(val);
+ return ColumnWidth.FromCharacters(8.43); // стандартная ширина
+ }
+ }
+ set
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var col = GetOrCreateColumnElement();
+ double widthInChars;
+
+ if (value.IsCharacterUnit)
+ {
+ // Если ширина задана в символах, используем напрямую
+ widthInChars = value.CharacterValue;
+ }
+ else
+ {
+ double targetCm = value.GetTargetCentimeters();
+ widthInChars = _writer.TryGetCalibrateCoeff(targetCm, out var closestCw)
+ ? closestCw : targetCm * (ColumnWidth.DefaultPointsPerChar / 28.3464566929);
+ }
+
+ col.Width = widthInChars;
+ col.CustomWidth = true;
+ }
+ }
+}
+
+ public IColumn SetNumberFormat(NumberFormatPattern format)
+ {
+ if (format == null) return this;
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ int fmtId = GetOrCreateNumberFormatId(format);
+ // Применяем ко всем ячейкам столбца
+ var sheetData = _sheet.GetSheetData();
+ foreach (var row in sheetData.Elements())
+ {
+ var cell = FindCellInRow(row, _colIndex);
+ cell?.StyleIndex = (uint)fmtId;
+ }
+ }
+ return this;
+ }
+
+ public ICell Cell(uint row)
+ {
+ return new ExcelCell(_writer, _sheet, row, _colIndex);
+ }
+
+ public IColumn Cell(uint row, Action edit)
+ {
+ var cell = Cell(row);
+ edit(cell);
+ return this;
+ }
+
+ public IColumn Cell(uint row, string value)
+ {
+ Cell(row).Set(value); return this;
+ }
+
+ public IColumn Cell(uint row, bool value)
+ {
+ Cell(row).Set(value); return this;
+ }
+
+ public IColumn Cell(uint row, string value, NumberFormatPattern? format = null)
+ {
+ Cell(row).Set(value, format); return this;
+ }
+
+ public IColumn Cell(uint row, DateTime value, NumberFormatPattern? format = null)
+ {
+ Cell(row).Set(value, format); return this;
+ }
+
+ public IColumn Cell(uint row, decimal value, NumberFormatPattern? format = null)
+ {
+ Cell(row).Set(value, format); return this;
+ }
+
+ public IColumn Cell(uint row, double value, NumberFormatPattern? format = null)
+ {
+ Cell(row).Set(value, format); return this;
+ }
+
+ public IColumn Cell(uint row, float value, NumberFormatPattern? format = null)
+ {
+ Cell(row).Set(value, format); return this;
+ }
+
+ public IColumn Cell(uint row, int value, NumberFormatPattern? format = null)
+ {
+ Cell(row).Set(value, format); return this;
+ }
+
+ public IColumn Cell(uint row, long value, NumberFormatPattern? format = null)
+ {
+ Cell(row).Set(value, format); return this;
+ }
+
+
+
+
+
+
+ public void ClearContents()
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ ClearColumnData(_colIndex);
+ }
+ }
+
+ public void ClearFormats()
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var sheetData = _sheet.GetSheetData();
+ foreach (var row in sheetData.Elements())
+ {
+ var cell = FindCellInRow(row, _colIndex);
+ cell?.StyleIndex = null;
+ }
+ }
+ }
+
+ public void Clear()
+ {
+ ClearContents();
+ ClearFormats();
+ }
+
+ public void Remove()
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ // Очищаем данные
+ ClearColumnData(_colIndex);
+ // Удаляем элемент Column, если он существует
+ DeleteColumnElement();
+ }
+ }
+
+ public IColumn CopyTo(uint index, out IColumn copiedColumn)
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ CopyColumnData(_colIndex, index);
+ copiedColumn = new ExcelColumn(_writer, _sheet, index);
+ return this;
+ }
+ }
+
+ public IColumn CopyTo(string index, out IColumn copiedColumn)
+ {
+ uint idx = CellAddressHelper.ColumnLetterToIndex(index);
+ return CopyTo(idx, out copiedColumn);
+ }
+
+ // Вспомогательные методы
+
+ private Column? GetColumnElement()
+ {
+ var cols = _sheet.Worksheet.GetFirstChild();
+ if (cols == null) return null;
+ foreach (Column col in cols.Elements())
+ {
+ if (col?.Min?.Value is { } min
+ && col?.Max?.Value is { } max
+ && min <= _colIndex
+ && max >= _colIndex)
+ return col;
+ }
+ return null;
+ }
+
+ private Column GetOrCreateColumnElement()
+ {
+ var existing = GetColumnElement();
+ 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 = _colIndex,
+ Max = _colIndex,
+ Width = 8.43,
+ CustomWidth = true
+ };
+ cols.Append(newCol);
+ return newCol;
+ }
+
+ private void DeleteColumnElement()
+ {
+ var col = GetColumnElement();
+ col?.Remove();
+ }
+
+ private void CopyColumnData(uint sourceCol, uint targetCol)
+ {
+ if (sourceCol == targetCol) return;
+ var sheetData = _sheet.GetSheetData();
+ // Сначала удаляем существующие данные в целевом столбце (если нужно перезаписать)
+ ClearColumnData(targetCol);
+ // Копируем значения и форматы из sourceCol в targetCol
+ foreach (var row in sheetData.Elements())
+ {
+ var sourceCell = FindCellInRow(row, sourceCol);
+ if (sourceCell != null)
+ {
+ var targetCell = FindCellInRow(row, targetCol);
+ if (targetCell == null)
+ {
+ targetCell = new Cell();
+ // Вставляем в нужное место (по порядку столбцов)
+ InsertCellInRow(row, targetCell, targetCol);
+ }
+ // Копируем содержимое и стиль
+ targetCell.DataType = sourceCell.DataType;
+ targetCell.CellValue = sourceCell.CellValue?.CloneNode(true) as CellValue;
+ targetCell.CellFormula = sourceCell.CellFormula?.CloneNode(true) as CellFormula;
+ targetCell.StyleIndex = sourceCell.StyleIndex;
+ // Если есть InlineString, клонируем
+ if (sourceCell.InlineString != null)
+ targetCell.InlineString = (InlineString)sourceCell.InlineString.CloneNode(true);
+ }
+ }
+ // Копируем ширину столбца
+ var sourceColElem = GetColumnElementForIndex(sourceCol);
+ if (sourceColElem?.Width is { } width)
+ {
+ var targetColElem = GetOrCreateColumnElementForIndex(targetCol);
+ targetColElem.Width = width;
+ targetColElem.CustomWidth = sourceColElem.CustomWidth;
+ }
+ }
+
+ private void ClearColumnData(uint col)
+ {
+ var sheetData = _sheet.GetSheetData();
+ foreach (var row in sheetData.Elements().ToList())
+ {
+ var cell = FindCellInRow(row, col);
+ cell?.Remove();
+ }
+ }
+
+ private Cell? FindCellInRow(Row row, uint colIndex)
+ {
+ foreach (var cell in row.Elements())
+ {
+ string cellRef = cell.CellReference?.Value ?? string.Empty;
+ if (TryParseCellReference(cellRef, out uint _, out uint colIdx) && colIdx == colIndex)
+ return cell;
+ }
+ return null;
+ }
+
+ private void InsertCellInRow(Row row, Cell cell, uint colIndex)
+ {
+ string newRef = NumberToColumnLetter(colIndex) + (row.RowIndex?.Value ?? 1).ToString();
+ cell.CellReference = newRef;
+ // Вставляем в правильном порядке (по возрастанию столбцов)
+ bool inserted = false;
+ foreach (var existing in row.Elements().ToList())
+ {
+ if (TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) &&
+ existingCol > colIndex)
+ {
+ existing.InsertBeforeSelf(cell);
+ inserted = true;
+ break;
+ }
+ }
+ if (!inserted)
+ row.Append(cell);
+ }
+
+ private Column? GetColumnElementForIndex(uint col)
+ {
+ var cols = _sheet.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;
+ }
+
+ private 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;
+ }
+
+ private int GetOrCreateNumberFormatId(NumberFormatPattern format)
+ {
+ return _writer.GetOrCreateCellFormatId(numberFormat: format);
+ }
+
+ private static bool TryParseCellReference(string reference, out uint row, out uint col)
+ {
+ row = 0; col = 0;
+ if (string.IsNullOrEmpty(reference)) return false;
+ int i = 0;
+ while (i < reference.Length && char.IsLetter(reference[i])) i++;
+ if (i == 0) return false;
+ string colPart = reference.Substring(0, i);
+ string rowPart = reference.Substring(i);
+ if (!uint.TryParse(rowPart, out row)) return false;
+ col = CellAddressHelper.ColumnLetterToIndex(colPart);
+ return true;
+ }
+
+ private static string NumberToColumnLetter(uint col)
+ {
+ if (col == 0) throw new ArgumentException("Column number must be > 0");
+ string result = "";
+ while (col > 0)
+ {
+ col--;
+ result = (char)('A' + (col % 26)) + result;
+ col /= 26;
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs
new file mode 100644
index 0000000..e78e922
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExcelRange.cs
@@ -0,0 +1,886 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Внутренняя реализация .
+///
+internal sealed class ExcelRange : IRange
+{
+ private readonly ExcelWriter _writer;
+ private readonly ExcelSheet _sheet;
+ private uint _rowStart, _rowEnd;
+ private 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;
+ }
+ }
+ }
+
+
+
+
+
+
+
+ ///
+ /// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить").
+ /// Исходный диапазон очищается, а текущий объект 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));
+ }
+
+ private enum CopyOrder
+ {
+ Any,
+ LeftToRight,
+ RightToLeft,
+ TopToBottom,
+ BottomToTop
+ }
+
+ /// Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода.
+ private 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);
+ }
+ }
+ }
+
+ /// Строит словарь строк для указанных индексов строк.
+ private 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;
+ }
+
+ /// Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке).
+ private 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;
+ }
+
+ /// Проверяет, пересекаются ли два прямоугольных диапазона.
+ private 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);
+ }
+
+ /// Вставляет ячейку в указанную позицию, предварительно удаляя существующую.
+ private void InsertCellAt(uint row, uint col, Cell cell)
+ {
+ DeleteCellAt(row, col);
+ var sheetData = _sheet.GetSheetData();
+ var rowElement = GetOrCreateRowElement(sheetData, row);
+ InsertCellInRow(rowElement, cell, col);
+ }
+
+ /// Удаляет ячейку, если она существует.
+ private 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();
+ }
+
+ /// Получает или создаёт строку с указанным индексом.
+ private 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 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);
+ }
+
+ /// Очищает данные (содержимое) в указанном диапазоне.
+ private 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);
+ }
+
+ public IRange SetNumberFormat(NumberFormatPattern format)
+ {
+ if (format == null) return this;
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ int fmtId = GetOrCreateNumberFormatId(format);
+ for (uint r = _rowStart; r <= _rowEnd; r++)
+ {
+ for (uint c = _colStart; c <= _colEnd; c++)
+ {
+ var cell = GetCellInternal(r, c);
+ cell?.StyleIndex = (uint)fmtId;
+ }
+ }
+ }
+ return this;
+ }
+
+ ///
+ public IRange SetCellAlign(CellAlign align)
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ int styleIndex = _writer.GetOrCreateCellFormatId(
+ numberFormat: null,
+ font: null,
+ fill: null,
+ border: null,
+ align: align
+ );
+ ApplyStyleToRange((uint)styleIndex);
+ return this;
+ }
+ }
+
+ ///
+ public IRange SetCellBorder(CellBorder border)
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ int styleIndex = _writer.GetOrCreateCellFormatId(
+ numberFormat: null,
+ font: null,
+ fill: null,
+ border: border,
+ align: null
+ );
+ ApplyStyleToRange((uint)styleIndex);
+ return this;
+ }
+ }
+
+ ///
+ public IRange SetCellFill(CellFill fill)
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ int styleIndex = _writer.GetOrCreateCellFormatId(
+ numberFormat: null,
+ font: null,
+ fill: fill,
+ border: null,
+ align: null
+ );
+ ApplyStyleToRange((uint)styleIndex);
+ return this;
+ }
+ }
+
+ ///
+ public IRange SetCellFont(CellFont font)
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ int styleIndex = _writer.GetOrCreateCellFormatId(
+ numberFormat: null,
+ font: font,
+ fill: null,
+ border: null,
+ align: null
+ );
+ ApplyStyleToRange((uint)styleIndex);
+ return this;
+ }
+ }
+
+ ///
+ /// Применяет стиль ко всем ячейкам диапазона.
+ ///
+ /// Индекс стиля для применения.
+ /// Если true, создаёт недостающие ячейки (по умолчанию true).
+ private 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.Set(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.Set(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);
+ }
+
+ // Вспомогательные методы
+
+ private Cell? GetCellInternal(uint row, uint col)
+ {
+ var sheetData = _sheet.GetSheetData();
+ var rowElement = FindRowElement(sheetData, row);
+ if (rowElement == null) return null;
+ return FindCellInRow(rowElement, col);
+ }
+
+ private 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;
+ }
+ }
+ }
+
+ private 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;
+ }
+
+ 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 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 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 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;
+ }
+
+ private 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;
+ }
+
+ 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;
+ }
+
+ // создаёт стиль только с числовым форматом
+ private int GetOrCreateNumberFormatId(NumberFormatPattern format)
+ {
+ return _writer.GetOrCreateCellFormatId(numberFormat: format);
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs
new file mode 100644
index 0000000..6dea9b5
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExcelRow.cs
@@ -0,0 +1,393 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Внутренняя реализация .
+///
+internal sealed class ExcelRow : IRow
+{
+ private readonly ExcelWriter _writer;
+ private readonly ExcelSheet _sheet;
+ private uint _rowIndex;
+
+ internal ExcelRow(ExcelWriter writer, ExcelSheet sheet, uint rowIndex)
+ {
+ _writer = writer;
+ _sheet = sheet;
+ _rowIndex = rowIndex;
+ }
+
+ ///
+ public uint Index => _rowIndex;
+
+
+
+ /// Устанавливает высоту строки без создания объекта IRow.
+ internal static void SetHeight(ExcelWriter writer, ExcelSheet sheet, uint rowIndex, RowHeight height)
+ {
+ writer.ThrowIfDisposed();
+ lock (writer._syncLock)
+ {
+ var row = GetOrCreateRowElementInternal(sheet, rowIndex);
+ row.Height = height.Points;
+ row.CustomHeight = true;
+ }
+ }
+
+ /// Возвращает высоту строки.
+ internal static RowHeight GetHeight(ExcelWriter writer, ExcelSheet sheet, uint rowIndex)
+ {
+ writer.ThrowIfDisposed();
+ lock (writer._syncLock)
+ {
+ var row = GetRowElementInternal(sheet, rowIndex);
+ if (row is { } r && r.CustomHeight?.HasValue == true && r.Height?.Value is { } hei)
+ return RowHeight.FromPoints(hei);
+ return RowHeight.FromPoints(15.0); // стандартная высота
+ }
+ }
+
+ // Вспомогательные методы
+ private static Row? GetRowElementInternal(ExcelSheet sheet, uint rowIndex)
+ {
+ var sheetData = sheet.GetSheetData();
+ foreach (var row in sheetData.Elements())
+ if (row.RowIndex?.Value == rowIndex) return row;
+ return null;
+ }
+
+ private static Row GetOrCreateRowElementInternal(ExcelSheet sheet, uint rowIndex)
+ {
+ var existing = GetRowElementInternal(sheet, rowIndex);
+ if (existing != null) return existing;
+
+ var sheetData = sheet.GetSheetData();
+ var newRow = new Row { RowIndex = rowIndex };
+ InsertRowElementInternal(sheetData, newRow, rowIndex);
+ return newRow;
+ }
+
+ private static void InsertRowElementInternal(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);
+ }
+
+
+
+ ///
+ public IRow MoveTo(uint index)
+ {
+ if (index == _rowIndex) return this;
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var sheetData = _sheet.GetSheetData();
+ // Находим элемент строки
+ var rowElement = FindRowElement(sheetData, _rowIndex);
+ if (rowElement == null)
+ {
+ // Пустая строка — просто меняем индекс
+ _rowIndex = index;
+ return this;
+ }
+ // Удаляем из текущей позиции
+ rowElement.Remove();
+ // Вставляем в новую позицию
+ InsertRowElement(sheetData, rowElement, index);
+ // Обновляем RowIndex в элементе
+ rowElement.RowIndex = index;
+ _rowIndex = index;
+ }
+ return this;
+ }
+
+ ///
+ public RowHeight Height
+ {
+ get
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var rowElement = GetOrCreateRowElement();
+ if (rowElement.Height?.Value is double h && rowElement.CustomHeight?.Value == true)
+ return RowHeight.FromPoints(h);
+ // Стандартная высота, если не задана (обычно 15 пунктов)
+ return RowHeight.FromPoints(15.0);
+ }
+ }
+ set
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var rowElement = GetOrCreateRowElement();
+ rowElement.Height = value.Points;
+ rowElement.CustomHeight = true;
+ }
+ }
+ }
+
+ ///
+ public IRow SetNumberFormat(NumberFormatPattern format)
+ {
+ if (format == null) return this;
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ // Находим все ячейки в этой строке и устанавливаем формат
+ var sheetData = _sheet.GetSheetData();
+ var rowElement = FindRowElement(sheetData, _rowIndex);
+ if (rowElement == null) return this;
+ int formatIndex = GetOrCreateNumberFormatId(format);
+ foreach (var cell in rowElement.Elements())
+ cell.StyleIndex = (uint)formatIndex;
+ }
+ return this;
+ }
+
+ ///
+ public ICell Cell(uint col) => new ExcelCell(_writer, _sheet, _rowIndex, col);
+
+ ///
+ public IRow Cell(uint col, Action edit)
+ {
+ edit(Cell(col)); return this;
+ }
+
+ public IRow Cell(uint col, string value)
+ {
+ Cell(col).Set(value); return this;
+ }
+
+ public IRow Cell(uint col, bool value)
+ {
+ Cell(col).Set(value); return this;
+ }
+
+ public IRow Cell(uint col, string value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(uint col, DateTime value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(uint col, decimal value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(uint col, double value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(uint col, float value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(uint col, int value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(uint col, long value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(string col, Action edit)
+ {
+ edit(Cell(col)); return this;
+ }
+
+ public IRow Cell(string col, string value)
+ {
+ Cell(col).Set(value); return this;
+ }
+
+ public IRow Cell(string col, bool value)
+ {
+ Cell(col).Set(value); return this;
+ }
+
+ public IRow Cell(string col, string value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(string col, DateTime value, NumberFormatPattern? format = null)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IRow Cell(string col, decimal value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(string col, double value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(string col, float value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(string col, int value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+ public IRow Cell(string col, long value, NumberFormatPattern? format = null)
+ {
+ Cell(col).Set(value, format); return this;
+ }
+
+
+
+
+ ///
+ public ICell Cell(string col) => Cell(CellAddressHelper.ColumnLetterToIndex(col));
+
+ ///
+ public IRow EditCell(string col, Action edit)
+ {
+ edit(Cell(col)); return this;
+ }
+
+ ///
+ public void ClearContents()
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var rowElement = FindRowElement(_sheet.GetSheetData(), _rowIndex);
+ if (rowElement == null) return;
+ foreach (var cell in rowElement.Elements().ToList())
+ {
+ cell.Remove();
+ }
+ }
+ }
+
+ ///
+ public void ClearFormats()
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var rowElement = FindRowElement(_sheet.GetSheetData(), _rowIndex);
+ if (rowElement == null) return;
+ foreach (var cell in rowElement.Elements())
+ {
+ cell.StyleIndex = null;
+ }
+ }
+ }
+
+ ///
+ public void Clear()
+ {
+ ClearContents();
+ ClearFormats();
+ }
+
+ ///
+ public void Remove()
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var sheetData = _sheet.GetSheetData();
+ var rowElement = FindRowElement(sheetData, _rowIndex);
+ rowElement?.Remove();
+ // Сдвиг индексов последующих строк вниз в Open XML не требуется, т.к. они имеют свои RowIndex.
+ // Но при последующем доступе по индексам нужно будет корректировать.
+ }
+ }
+
+ ///
+ public IRow CopyTo(uint index, out IRow copiedRow)
+ {
+ _writer.ThrowIfDisposed();
+ lock (_writer._syncLock)
+ {
+ var sourceRow = FindRowElement(_sheet.GetSheetData(), _rowIndex);
+ if (sourceRow == null)
+ {
+ copiedRow = new ExcelRow(_writer, _sheet, index);
+ return this;
+ }
+ // Клонируем элемент строки
+ var clonedRow = (Row)sourceRow.CloneNode(true);
+ clonedRow.RowIndex = index;
+ // Вставляем в новую позицию
+ var sheetData = _sheet.GetSheetData();
+ InsertRowElement(sheetData, clonedRow, index);
+ copiedRow = new ExcelRow(_writer, _sheet, index);
+ return this;
+ }
+ }
+
+ // Вспомогательные методы
+
+ private Row GetOrCreateRowElement()
+ {
+ var sheetData = _sheet.GetSheetData();
+ var existing = FindRowElement(sheetData, _rowIndex);
+ if (existing != null) return existing;
+ var newRow = new Row { RowIndex = _rowIndex };
+ InsertRowElement(sheetData, newRow, _rowIndex);
+ return newRow;
+ }
+
+ private static Row? FindRowElement(SheetData sheetData, uint rowIndex)
+ {
+ // Поиск по атрибуту RowIndex. В Open XML строки могут идти не по порядку.
+ foreach (var row in sheetData.Elements())
+ {
+ if (row.RowIndex?.Value == rowIndex)
+ return row;
+ }
+ return null;
+ }
+
+ private static void InsertRowElement(SheetData sheetData, Row row, uint rowIndex)
+ {
+ // Вставка с сохранением сортировки по 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 int GetOrCreateNumberFormatId(NumberFormatPattern format)
+ {
+ // Создаём стиль, содержащий только числовой формат, и возвращаем его индекс
+ return _writer.GetOrCreateCellFormatId(numberFormat: format);
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs
new file mode 100644
index 0000000..2bfbc8c
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExcelRun.cs
@@ -0,0 +1,25 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Внутренняя реализация для хранения форматированного фрагмента текста.
+/// Минимизирует аллокации, не использует рефлексию, все поля доступны напрямую внутри сборки.
+///
+internal sealed class ExcelRun : IRun
+{
+ private string _text = string.Empty;
+ private RunFormat? _format;
+
+ ///
+ public string Text
+ {
+ get => _text;
+ set => _text = value ?? string.Empty;
+ }
+
+ ///
+ public RunFormat? Format
+ {
+ get => _format;
+ set => _format = value;
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs
new file mode 100644
index 0000000..4d4810d
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs
@@ -0,0 +1,277 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Внутренняя реализация .
+///
+internal sealed class ExcelSheet : ISheet
+{
+ internal readonly ExcelWriter Book;
+ internal readonly Sheet SheetElement;
+ internal readonly Worksheet Worksheet;
+ internal readonly uint SheetId;
+
+ internal ExcelSheet(ExcelWriter book, Sheet sheetElement, uint sheetId)
+ {
+ Book = book;
+ SheetElement = sheetElement;
+ SheetId = sheetId;
+
+ string partId = SheetElement.Id?.Value ?? string.Empty;
+ if (string.IsNullOrEmpty(partId))
+ throw new InvalidOperationException("Sheet has no relationship ID");
+ if (Book._doc.WorkbookPart?.GetPartById(partId) is not WorksheetPart part)
+ throw new InvalidOperationException("WorksheetPart not found");
+ if (part.Worksheet is not Worksheet worksheet)
+ throw new InvalidOperationException("WorksheetPart not found");
+ Worksheet = worksheet;
+ }
+
+ public int Index => (int)SheetId;
+ public string Name => SheetElement.Name?.Value ?? string.Empty;
+
+ public bool TrySetName(string name)
+ {
+ if (string.IsNullOrEmpty(name)) return false;
+ Book.ThrowIfDisposed();
+ SheetElement.Name = name;
+ return true;
+ }
+
+ public IRow Row(uint row) => new ExcelRow(Book, this, row);
+ public ISheet Row(uint row, Action edit)
+ {
+ edit(Row(row));
+ return this;
+ }
+
+ public IColumn Col(uint col) => new ExcelColumn(Book, this, col);
+ public ISheet Col(uint col, Action edit)
+ {
+ edit(Col(col));
+ return this;
+ }
+
+ public IColumn Col(string col) => Col(CellAddressHelper.ColumnLetterToIndex(col));
+ public ISheet Col(string col, Action edit)
+ {
+ edit(Col(col));
+ return this;
+ }
+
+ public ICell Cell(uint row, uint col) => new ExcelCell(Book, this, row, col);
+ public ISheet Cell(uint row, uint col, Action edit)
+ {
+ edit(Cell(row, col));
+ return this;
+ }
+
+ public ISheet Cell(uint row, uint col, string value)
+ {
+ Cell(row, col).Set(value); return this;
+ }
+
+ public ISheet Cell(uint row, uint col, bool value)
+ {
+ Cell(row, col).Set(value); return this;
+ }
+
+ public ISheet Cell(uint row, uint col, string formula, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(formula, format); return this;
+ }
+
+ public ISheet Cell(uint row, uint col, DateTime value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, uint col, decimal value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, uint col, double value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, uint col, float value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, uint col, int value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, uint col, long value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ICell Cell(uint row, string col) => Cell(row, CellAddressHelper.ColumnLetterToIndex(col));
+ public ISheet Cell(uint row, string col, Action edit)
+ {
+ edit(Cell(row, col));
+ return this;
+ }
+
+ public ISheet Cell(uint row, string col, string value)
+ {
+ Cell(row, col).Set(value); return this;
+ }
+
+ public ISheet Cell(uint row, string col, bool value)
+ {
+ Cell(row, col).Set(value); return this;
+ }
+
+ public ISheet Cell(uint row, string col, string formula, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(formula, format); return this;
+ }
+
+ public ISheet Cell(uint row, string col, DateTime value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, string col, decimal value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, string col, double value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, string col, float value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, string col, int value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public ISheet Cell(uint row, string col, long value, NumberFormatPattern? format = null)
+ {
+ Cell(row, col).Set(value, format); return this;
+ }
+
+ public void ClearContents()
+ {
+ var sheetData = GetSheetData();
+ foreach (var row in sheetData.Elements().ToList())
+ {
+ foreach (var cell in row.Elements().ToList())
+ cell.Remove();
+ }
+ }
+
+ public void ClearFormats()
+ {
+ var sheetData = GetSheetData();
+ foreach (var row in sheetData.Elements())
+ {
+ foreach (var cell in row.Elements())
+ cell.StyleIndex = null;
+ }
+ }
+
+ public void Clear()
+ {
+ ClearContents();
+ ClearFormats();
+ }
+
+ public void Remove() => Book.TryRemoveSheet(this);
+
+ // Вспомогательные методы
+
+ internal SheetData GetSheetData()
+ {
+ var worksheet = Worksheet;
+ var sheetData = worksheet.GetFirstChild();
+ if (sheetData == null)
+ {
+ sheetData = new SheetData();
+ worksheet.Append(sheetData);
+ }
+ return sheetData;
+ }
+
+
+
+
+ #region Range Operations
+
+ ///
+ public IRange RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol)
+ {
+ return new ExcelRange(Book, this, startRow, startCol, endRow, endCol);
+ }
+
+ ///
+ public IRange RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol)
+ {
+ uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol);
+ uint endColIdx = CellAddressHelper.ColumnLetterToIndex(endCol);
+ return new ExcelRange(Book, this, startRow, startColIdx, endRow, endColIdx);
+ }
+
+ ///
+ public ISheet RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol, Action edit)
+ {
+ var range = RangeByIndexes(startRow, startCol, endRow, endCol);
+ edit(range);
+ return this;
+ }
+
+ ///
+ public ISheet RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol, Action edit)
+ {
+ var range = RangeByIndexes(startRow, startCol, endRow, endCol);
+ edit(range);
+ return this;
+ }
+
+ ///
+ public IRange RangeByLength(uint startRow, uint startCol, uint rows, uint cols)
+ {
+ if (rows == 0 || cols == 0)
+ throw new ArgumentException("Rows and columns must be greater than 0");
+ uint endRow = startRow + rows - 1;
+ uint endCol = startCol + cols - 1;
+ return new ExcelRange(Book, this, startRow, startCol, endRow, endCol);
+ }
+
+ ///
+ public IRange RangeByLength(uint startRow, string startCol, uint rows, uint cols)
+ {
+ uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol);
+ return RangeByLength(startRow, startColIdx, rows, cols);
+ }
+
+ ///
+ public ISheet RangeByLength(uint startRow, uint startCol, uint rows, uint cols, Action edit)
+ {
+ var range = RangeByLength(startRow, startCol, rows, cols);
+ edit(range);
+ return this;
+ }
+
+ ///
+ public ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action edit)
+ {
+ var range = RangeByLength(startRow, startCol, rows, cols);
+ edit(range);
+ return this;
+ }
+
+ #endregion
+
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/Interfaces.cs b/QWERTYkez.ExcelProcessor/Editors/Interfaces.cs
new file mode 100644
index 0000000..e61bbcc
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/Interfaces.cs
@@ -0,0 +1,817 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+/// Представляет книгу Excel
+public interface IBook
+{
+ /// Возвращает список всех листов в книге.
+ IReadOnlyList GetSheets();
+
+ /// Возвращает лист по имени или null, если лист не найден.
+ /// Имя листа (регистр учитывается).
+ ISheet? Sheet(string name);
+
+ /// Пытается получить лист по имени.
+ /// Имя листа.
+ /// Найденный лист или null.
+ /// true, если лист найден, иначе false.
+ bool TryGetSheet(string name, out ISheet sheet);
+
+ /// Пытается добавить новый лист с указанным именем и выполняет его настройку.
+ /// Имя нового листа (должно быть уникальным в книге).
+ /// Действие для редактирования листа (может быть null).
+ /// true, если лист успешно создан, иначе false (например, имя уже существует).
+ bool TryAddSheet(string name, Action? edit = null);
+
+ /// Удаляет лист по имени.
+ /// Имя удаляемого листа.
+ /// true, если лист существовал и был удалён, иначе false.
+ bool TryRemoveSheet(string name);
+
+ /// Удаляет лист.
+ /// Удаляемый лист.
+ /// true, если лист был удалён, иначе false.
+ bool TryRemoveSheet(ISheet sheet);
+
+ /// Возвращает список всех числовых форматов, определённых в книге (встроенные и пользовательские).
+ IReadOnlyList GetNumberFormats();
+
+ /// Создаёт новый пользовательский числовой формат (если формат с таким кодом уже существует, может вернуть существующий).
+ /// Код формата (например, "# ##0,00").
+ /// Объект формата, привязанный к книге.
+ NumberFormatPattern CreateNumberFormat(string format);
+}
+
+/// Представляет лист Excel
+public interface ISheet
+{
+ /// Индекс листа в книге (начиная с 1).
+ int Index { get; }
+
+ /// Имя листа (уникальное в книге).
+ string Name { get; }
+
+ /// Пытается изменить имя листа.
+ /// Новое имя (не должно совпадать с существующими).
+ /// true, если переименование выполнено, иначе false.
+ bool TrySetName(string name);
+
+ /// Возвращает строку по её индексу (начиная с 1). Если строка отсутствует, создаёт пустую.
+ IRow Row(uint row);
+
+ /// Редактирует строку, применяя делегат, и возвращает текущий лист (fluent).
+ /// Индекс строки (начиная с 1).
+ /// Действие над строкой.
+ ISheet Row(uint row, Action edit);
+
+ /// Возвращает столбец по его индексу (начиная с 1). Если столбец отсутствует, создаёт пустой.
+ IColumn Col(uint col);
+
+ /// Редактирует столбец по индексу.
+ ISheet Col(uint col, Action edit);
+
+ /// Возвращает столбец по буквенному обозначению (например, "A", "AB").
+ IColumn Col(string col);
+
+ /// Редактирует столбец по буквенному обозначению.
+ ISheet Col(string col, Action edit);
+
+ /// Возвращает диапазон ячеек по начальным и конечным индексам строк и столбцов.
+ IRange RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol);
+
+ /// Возвращает диапазон ячеек по начальным и конечным координатам с буквенным обозначением столбцов.
+ IRange RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol);
+
+ /// Редактирует диапазон, заданный начальными и конечными индексами.
+ ISheet RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol, Action edit);
+
+ /// Редактирует диапазон, заданный начальными и конечными координатами с буквенными столбцами.
+ ISheet RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol, Action edit);
+
+ /// Возвращает диапазон, начиная с указанной ячейки, заданной размером (строки x столбцы).
+ IRange RangeByLength(uint startRow, uint startCol, uint rows, uint cols);
+
+ /// Возвращает диапазон по начальной ячейке и размеру с буквенным обозначением столбца.
+ IRange RangeByLength(uint startRow, string startCol, uint rows, uint cols);
+
+ /// Редактирует диапазон, заданный начальной ячейкой и размером.
+ ISheet RangeByLength(uint startRow, uint startCol, uint rows, uint cols, Action edit);
+
+ /// Редактирует диапазон, заданный начальной ячейкой и размером (буква столбца).
+ ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action edit);
+
+ /// Возвращает ячейку по номеру строки и столбца (оба начиная с 1).
+ ICell Cell(uint row, uint col);
+
+ /// Редактирует ячейку по строке и столбцу.
+ ISheet Cell(uint row, uint col, Action edit);
+
+ /// Устанавливает строковое значение в ячейку.
+ ISheet Cell(uint row, uint col, string value);
+
+ /// Устанавливает логическое значение в ячейку.
+ ISheet Cell(uint row, uint col, bool value);
+
+ /// Устанавливает формулу в ячейку с указанным числовым форматом.
+ ISheet Cell(uint row, uint col, string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает дату в ячейку с указанным числовым форматом.
+ ISheet Cell(uint row, uint col, DateTime value, NumberFormatPattern? format = null);
+
+ /// Устанавливает десятичное число в ячейку с указанным числовым форматом.
+ ISheet Cell(uint row, uint col, decimal value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число двойной точности в ячейку с указанным числовым форматом.
+ ISheet Cell(uint row, uint col, double value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число с плавающей точкой в ячейку с указанным числовым форматом.
+ ISheet Cell(uint row, uint col, float value, NumberFormatPattern? format = null);
+
+ /// Устанавливает целое число в ячейку с указанным числовым форматом.
+ ISheet Cell(uint row, uint col, int value, NumberFormatPattern? format = null);
+
+ /// Устанавливает длинное целое число в ячейку с указанным числовым форматом.
+ ISheet Cell(uint row, uint col, long value, NumberFormatPattern? format = null);
+
+ /// Возвращает ячейку по строке и букве столбца (например, 1, "A").
+ ICell Cell(uint row, string col);
+
+ /// Редактирует ячейку по строке и букве столбца.
+ ISheet Cell(uint row, string col, Action edit);
+
+ /// Устанавливает строковое значение в ячейку по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, string value);
+
+ /// Устанавливает логическое значение в ячейку по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, bool value);
+
+ /// Устанавливает формулу в ячейку с числовым форматом по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает дату в ячейку по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, DateTime value, NumberFormatPattern? format = null);
+
+ /// Устанавливает десятичное число в ячейку по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, decimal value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число двойной точности в ячейку по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, double value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число с плавающей точкой в ячейку по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, float value, NumberFormatPattern? format = null);
+
+ /// Устанавливает целое число в ячейку по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, int value, NumberFormatPattern? format = null);
+
+ /// Устанавливает длинное целое число в ячейку по адресу (строка, буква столбца).
+ ISheet Cell(uint row, string col, long value, NumberFormatPattern? format = null);
+
+ /// Очищает всё содержимое листа (значения, формулы, но не форматирование).
+ void ClearContents();
+
+ /// Очищает всё форматирование на листе (стили, шрифты, границы, но оставляет значения).
+ void ClearFormats();
+
+ /// Очищает и содержимое, и форматирование листа.
+ void Clear();
+
+ /// Удаляет лист из книги
+ void Remove();
+}
+
+/// Представляет строку на листе.
+public interface IRow
+{
+ /// Индекс строки (начиная с 1).
+ uint Index { get; }
+
+ ///
+ /// Копирует текущую строку в указанную позицию (как "копировать-вставить" в Excel).
+ /// Исходная строка остаётся без изменений.
+ ///
+ /// Индекс строки (начиная с 1), в которую будет вставлена копия.
+ /// Существующие строки, начиная с этой позиции, сдвигаются вниз.
+ /// Возвращает новую строку-копию, расположенную по указанному индексу.
+ /// Текущий объект IRow для цепочки вызовов (fluent).
+ IRow CopyTo(uint index, out IRow copiedRow);
+
+ ///
+ /// Перемещает текущую строку в новую позицию (как "вырезать-вставить" в Excel).
+ /// Исходная строка удаляется, а перемещённая строка сохраняет все свои данные и форматирование.
+ ///
+ /// Новый индекс строки (начиная с 1).
+ /// Другие строки сдвигаются, освобождая место для перемещённой строки.
+ /// Тот же объект IRow, но уже с новым индексом (fluent).
+ IRow MoveTo(uint index);
+
+ /// Высота строки
+ RowHeight Height { get; set; }
+
+ /// Устанавливает числовой формат для всех ячеек строки.
+ IRow SetNumberFormat(NumberFormatPattern format);
+
+ /// Возвращает ячейку в заданном столбце (индекс с 1).
+ ICell Cell(uint col);
+
+ /// Редактирует ячейку в заданном столбце.
+ IRow Cell(uint col, Action edit);
+
+ /// Устанавливает строковое значение в ячейку столбца.
+ IRow Cell(uint col, string value);
+
+ /// Устанавливает логическое значение в ячейку столбца.
+ IRow Cell(uint col, bool value);
+
+ /// Устанавливает формулу в ячейку столбца с числовым форматом.
+ IRow Cell(uint col, string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает дату в ячейку столбца с числовым форматом.
+ IRow Cell(uint col, DateTime value, NumberFormatPattern? format = null);
+
+ /// Устанавливает десятичное число в ячейку столбца с числовым форматом.
+ IRow Cell(uint col, decimal value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число двойной точности в ячейку столбца с числовым форматом.
+ IRow Cell(uint col, double value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число с плавающей точкой в ячейку столбца с числовым форматом.
+ IRow Cell(uint col, float value, NumberFormatPattern? format = null);
+
+ /// Устанавливает целое число в ячейку столбца с числовым форматом.
+ IRow Cell(uint col, int value, NumberFormatPattern? format = null);
+
+ /// Устанавливает длинное целое число в ячейку столбца с числовым форматом.
+ IRow Cell(uint col, long value, NumberFormatPattern? format = null);
+
+ /// Возвращает ячейку по букве столбца.
+ ICell Cell(string col);
+
+ /// Редактирует ячейку по букве столбца.
+ IRow Cell(string col, Action edit);
+
+ /// Устанавливает строковое значение в ячейку по букве столбца.
+ IRow Cell(string col, string value);
+
+ /// Устанавливает логическое значение в ячейку по букве столбца.
+ IRow Cell(string col, bool value);
+
+ /// Устанавливает формулу в ячейку по букве столбца с числовым форматом.
+ IRow Cell(string col, string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает дату в ячейку по букве столбца с числовым форматом.
+ IRow Cell(string col, DateTime value, NumberFormatPattern? format = null);
+
+ /// Устанавливает десятичное число в ячейку по букве столбца с числовым форматом.
+ IRow Cell(string col, decimal value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число двойной точности в ячейку по букве столбца с числовым форматом.
+ IRow Cell(string col, double value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число с плавающей точкой в ячейку по букве столбца с числовым форматом.
+ IRow Cell(string col, float value, NumberFormatPattern? format = null);
+
+ /// Устанавливает целое число в ячейку по букве столбца с числовым форматом.
+ IRow Cell(string col, int value, NumberFormatPattern? format = null);
+
+ /// Устанавливает длинное целое число в ячейку по букве столбца с числовым форматом.
+ IRow Cell(string col, long value, NumberFormatPattern? format = null);
+
+ /// Очищает содержимое всех ячеек строки (значения, формулы).
+ void ClearContents();
+
+ /// Очищает форматирование всех ячеек строки.
+ void ClearFormats();
+
+ /// Очищает содержимое и форматирование строки.
+ void Clear();
+
+ /// Удаляет строку с листа (сдвигая нижние строки вверх).
+ void Remove();
+}
+
+/// Представляет столбец на листе.
+public interface IColumn
+{
+ /// Индекс столбца (начиная с 1).
+ uint Index { get; }
+
+ /// Буквенное обозначение столбца (например, "A", "Z", "AA").
+ string IndexLetter { get; }
+
+ ///
+ /// Копирует текущий столбец в указанную позицию по числовому индексу (как "копировать-вставить").
+ /// Исходный столбец остаётся без изменений.
+ ///
+ /// Числовой индекс столбца (начиная с 1), куда будет вставлена копия.
+ /// Существующие столбцы, начиная с этой позиции, сдвигаются вправо.
+ /// Возвращает новый столбец-копию, расположенный по указанному индексу.
+ /// Текущий объект IColumn для цепочки вызовов.
+ IColumn CopyTo(uint index, out IColumn copiedColumn);
+
+ ///
+ /// Копирует текущий столбец в указанную позицию по буквенному обозначению (как "копировать-вставить").
+ ///
+ /// Буквенное обозначение столбца (например, "D", "AA"), куда будет вставлена копия.
+ /// Возвращает новый столбец-копию.
+ /// Текущий объект IColumn.
+ IColumn CopyTo(string index, out IColumn copiedColumn);
+
+ ///
+ /// Перемещает текущий столбец в новую позицию по числовому индексу (как "вырезать-вставить").
+ /// Исходный столбец удаляется, а перемещённый столбец сохраняет свои данные и форматирование.
+ ///
+ /// Новый числовой индекс столбца (начиная с 1).
+ /// Тот же объект IColumn с новым индексом (fluent).
+ IColumn MoveTo(uint index);
+
+ ///
+ /// Перемещает текущий столбец в новую позицию по буквенному обозначению (как "вырезать-вставить").
+ ///
+ /// Новое буквенное обозначение столбца (например, "E").
+ /// Тот же объект IColumn с новым индексом.
+ IColumn MoveTo(string index);
+
+ /// Ширина столбца (чтение и запись).
+ ColumnWidth Width { get; set; }
+
+ /// Устанавливает числовой формат для всех ячеек столбца.
+ IColumn SetNumberFormat(NumberFormatPattern format);
+
+ /// Возвращает ячейку в заданной строке (индекс с 1).
+ ICell Cell(uint row);
+
+ /// Редактирует ячейку в заданной строке.
+ IColumn Cell(uint row, Action edit);
+
+ /// Устанавливает строковое значение в ячейку строки.
+ IColumn Cell(uint row, string value);
+
+ /// Устанавливает логическое значение в ячейку строки.
+ IColumn Cell(uint row, bool value);
+
+ /// Устанавливает формулу в ячейку строки с числовым форматом.
+ IColumn Cell(uint row, string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает дату в ячейку строки с числовым форматом.
+ IColumn Cell(uint row, DateTime value, NumberFormatPattern? format = null);
+
+ /// Устанавливает десятичное число в ячейку строки с числовым форматом.
+ IColumn Cell(uint row, decimal value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число двойной точности в ячейку строки с числовым форматом.
+ IColumn Cell(uint row, double value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число с плавающей точкой в ячейку строки с числовым форматом.
+ IColumn Cell(uint row, float value, NumberFormatPattern? format = null);
+
+ /// Устанавливает целое число в ячейку строки с числовым форматом.
+ IColumn Cell(uint row, int value, NumberFormatPattern? format = null);
+
+ /// Устанавливает длинное целое число в ячейку строки с числовым форматом.
+ IColumn Cell(uint row, long value, NumberFormatPattern? format = null);
+
+ /// Очищает содержимое всех ячеек столбца.
+ void ClearContents();
+
+ /// Очищает форматирование всех ячеек столбца.
+ void ClearFormats();
+
+ /// Очищает содержимое и форматирование столбца.
+ void Clear();
+
+ /// Удаляет столбец с листа (сдвигая правые столбцы влево).
+ void Remove();
+}
+
+/// Представляет прямоугольный диапазон ячеек на листе.
+public interface IRange
+{
+ /// Объединяет ячейки диапазона в одну (содержимое левой верхней ячейки сохраняется).
+ /// true, если объединение выполнено (диапазон не был объединён ранее).
+ bool TryMerge();
+
+ /// Разъединяет ранее объединённый диапазон (восстанавливает исходные ячейки, содержимое остаётся в левой верхней).
+ void Unmerge();
+
+ /// Проверяет, объединён ли диапазон (как единое целое).
+ bool IsMerged { get; }
+
+ /// Возвращает диапазон, представляющий объединённую область, если текущий диапазон является частью объединения.
+ IRange? GetMergedRange();
+
+ /// Пытается получить объединённый диапазон, содержащий текущий.
+ bool TryGetMergedRange(IRange range);
+
+ /// Номер начальной строки диапазона (1-based).
+ uint RowStart { get; }
+
+ /// Номер конечной строки.
+ uint RowEnd { get; }
+
+ /// Номер начального столбца.
+ uint ColStart { get; }
+
+ /// Номер конечного столбца.
+ uint ColEnd { get; }
+
+ /// Буквенное обозначение начального столбца.
+ string ColStartLetter { get; }
+
+ /// Буквенное обозначение конечного столбца.
+ string ColEndLetter { get; }
+
+ /// Количество строк в диапазоне.
+ uint Rows { get; }
+
+ /// Количество столбцов в диапазоне.
+ uint Cols { get; }
+
+ ///
+ /// Копирует текущий диапазон в новую позицию (верхний левый угол) – как "копировать-вставить".
+ /// Исходный диапазон остаётся неизменным.
+ ///
+ /// Номер строки (начиная с 1) для верхнего левого угла вставляемого диапазона.
+ /// Номер столбца (начиная с 1) для верхнего левого угла.
+ /// Возвращает новый диапазон-копию, расположенный по указанным координатам.
+ /// Текущий объект IRange (fluent).
+ IRange CopyTo(uint rowIndex, uint colIndex, out IRange copiedRange);
+
+ ///
+ /// Копирует текущий диапазон в новую позицию (верхний левый угол) с указанием столбца буквой – как "копировать-вставить".
+ /// Исходный диапазон остаётся неизменным.
+ ///
+ /// Номер строки (начиная с 1).
+ /// Буквенное обозначение столбца (например, "C").
+ /// Возвращает новый диапазон-копию.
+ /// Текущий объект IRange.
+ IRange CopyTo(uint rowIndex, string colIndex, out IRange copiedRange);
+
+ ///
+ /// Перемещает текущий диапазон в новую позицию (верхний левый угол) – как "вырезать-вставить".
+ /// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
+ ///
+ /// Номер строки для нового положения верхнего левого угла.
+ /// Номер столбца для нового положения.
+ /// Тот же объект IRange, но уже с новыми координатами (fluent).
+ IRange MoveTo(uint rowIndex, uint colIndex);
+
+ ///
+ /// Перемещает текущий диапазон в новую позицию (верхний левый угол) с указанием столбца буквой – как "вырезать-вставить".
+ /// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
+ ///
+ /// Номер строки.
+ /// Буквенное обозначение столбца.
+ /// Тот же объект IRange с новыми координатами.
+ IRange MoveTo(uint rowIndex, string colIndex);
+
+ /// Устанавливает числовой формат для всех ячеек диапазона.
+ IRange SetNumberFormat(NumberFormatPattern format);
+
+ /// Устанавливает выравнивание для всех ячеек диапазона.
+ IRange SetCellAlign(CellAlign format);
+
+ /// Устанавливает границы для всех ячеек диапазона.
+ IRange SetCellBorder(CellBorder format);
+
+ /// Устанавливает заливку для всех ячеек диапазона.
+ IRange SetCellFill(CellFill format);
+
+ /// Устанавливает шрифт для всех ячеек диапазона.
+ IRange SetCellFont(CellFont format);
+
+ /// Перечисляет все ячейки диапазона (по строкам).
+ IEnumerable Cells { get; }
+
+ /// Получает ячейку внутри диапазона по относительным координатам (начиная с 1).
+ bool GetSubCell(uint row, uint col, out ICell cell);
+
+ /// Редактирует ячейку внутри диапазона по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, Action edit);
+
+ /// Устанавливает строковое значение в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, string value);
+
+ /// Устанавливает логическое значение в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, bool value);
+
+ /// Устанавливает формулу в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает дату в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, DateTime value, NumberFormatPattern? format = null);
+
+ /// Устанавливает десятичное число в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, decimal value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число двойной точности в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, double value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число с плавающей точкой в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, float value, NumberFormatPattern? format = null);
+
+ /// Устанавливает целое число в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, int value, NumberFormatPattern? format = null);
+
+ /// Устанавливает длинное целое число в ячейку по относительным координатам.
+ bool TryEditSubCell(uint row, uint col, long value, NumberFormatPattern? format = null);
+
+ /// Получает ячейку по относительной строке и букве столбца.
+ bool GetSubCell(uint row, string col, out ICell cell);
+
+ /// Редактирует ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, Action edit);
+
+ /// Устанавливает строковое значение в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, string value);
+
+ /// Устанавливает логическое значение в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, bool value);
+
+ /// Устанавливает формулу в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает дату в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, DateTime value, NumberFormatPattern? format = null);
+
+ /// Устанавливает десятичное число в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, decimal value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число двойной точности в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, double value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число с плавающей точкой в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, float value, NumberFormatPattern? format = null);
+
+ /// Устанавливает целое число в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, int value, NumberFormatPattern? format = null);
+
+ /// Устанавливает длинное целое число в ячейку по относительной строке и букве столбца.
+ bool TryEditSubCell(uint row, string col, long value, NumberFormatPattern? format = null);
+
+ /// Очищает содержимое всех ячеек диапазона.
+ void ClearContents();
+
+ /// Очищает форматирование всех ячеек диапазона.
+ void ClearFormats();
+
+ /// Очищает и содержимое, и форматирование.
+ void Clear();
+}
+
+/// Представляет одну ячейку на листе.
+public interface ICell
+{
+ /// Проверяет, входит ли ячейка в объединённый диапазон.
+ bool IsMerged { get; }
+
+ /// Возвращает объединённый диапазон, если ячейка объединена, иначе null.
+ IRange? GetMergedRange();
+
+ /// Пытается получить объединённый диапазон, содержащий эту ячейку.
+ bool TryGetMergedRange(IRange range);
+
+ /// Номер строки (1-based).
+ uint Row { get; }
+
+ /// Номер столбца (1-based).
+ uint Col { get; }
+
+ /// Буква столбца.
+ string ColLetter { get; }
+
+ ///
+ /// Копирует текущую ячейку в указанную позицию (как "копировать-вставить").
+ /// Исходная ячейка остаётся без изменений.
+ ///
+ /// Номер строки (начиная с 1), куда будет вставлена копия.
+ /// Номер столбца (начиная с 1).
+ /// Возвращает новую ячейку-копию.
+ /// Текущий объект ICell для цепочки вызовов.
+ ICell CopyTo(uint rowIndex, uint colIndex, out ICell copiedCell);
+
+ ///
+ /// Копирует текущую ячейку в указанную позицию с буквенным обозначением столбца (как "копировать-вставить").
+ /// Исходная ячейка остаётся без изменений.
+ ///
+ /// Номер строки.
+ /// Буквенное обозначение столбца (например, "B").
+ /// Возвращает новую ячейку-копию.
+ /// Текущий объект ICell.
+ ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell);
+
+ ///
+ /// Перемещает текущую ячейку в новую позицию (как "вырезать-вставить").
+ /// Исходная ячейка очищается, а текущий объект ICell перемещается в новое место.
+ ///
+ /// Новый номер строки.
+ /// Новый номер столбца.
+ /// Тот же объект ICell с новыми координатами (fluent).
+ ICell MoveTo(uint rowIndex, uint colIndex);
+
+ ///
+ /// Перемещает текущую ячейку в новую позицию с буквенным обозначением столбца (как "вырезать-вставить").
+ /// Исходная ячейка очищается, а текущий объект ICell перемещается в новое место.
+ ///
+ /// Новый номер строки.
+ /// Буквенное обозначение столбца.
+ /// Тот же объект ICell с новыми координатами.
+ ICell MoveTo(uint rowIndex, string colIndex);
+
+ /// Высота строки, содержащей ячейку (чтение и запись).
+ RowHeight Height { get; set; }
+
+ /// Ширина столбца, содержащего ячейку.
+ ColumnWidth Width { get; set; }
+
+ /// Проверяет, содержит ли ячейка числовое значение (целое или с плавающей точкой).
+ bool IsNumber { get; }
+
+ /// Проверяет, является ли содержимое логическим (TRUE/FALSE).
+ bool IsBoolean { get; }
+
+ /// Проверяет, содержит ли ячейка код ошибки (например, #DIV/0!).
+ bool IsError { get; }
+
+ /// Проверяет, интерпретируется ли значение как дата (по числовому формату).
+ bool IsDate { get; }
+
+ /// Проверяет, заблокирована ли ячейка (атрибут защиты).
+ bool IsLocked { get; }
+
+ /// Проверяет, содержит ли ячейка формулу.
+ bool HasFormula { get; }
+
+ /// Возвращает текстовое представление содержимого ячейки (как оно отображается).
+ string GetString();
+
+ /// Возвращает числовой формат ячейки.
+ NumberFormatPattern? GetNumberFormat();
+
+ /// Возвращает выравнивание текста ячейки.
+ CellAlign GetCellAlign();
+
+ /// Возвращает границы ячейки.
+ CellBorder GetCellBorder();
+
+ /// Возвращает заливку ячейки.
+ CellFill GetCellFill();
+
+ /// Возвращает шрифт ячейки.
+ CellFont GetCellFont();
+
+ /// Пытается извлечь логическое значение.
+ bool TryGetBoolean(out bool value);
+
+ /// Возвращает логическое значение или null, если тип не соответствует.
+ bool? GetBoolean();
+
+ /// Пытается извлечь дату и время.
+ bool TryGetDate(out DateTime value);
+
+ /// Возвращает дату или null.
+ DateTime? TryGetDate();
+
+ /// Пытается извлечь число (double).
+ bool TryGetNumber(out double value);
+
+ /// Возвращает число или null.
+ double? TryGetNumber();
+
+ /// Пытается установить формулу (без вычисленного значения).
+ /// Текст формулы (например, "SUM(A1:A5)").
+ /// Необязательный числовой формат для результата.
+ bool TrySet(string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает формулу (выбрасывает исключение при ошибке).
+ ICell Set(string formula, NumberFormatPattern? format = null);
+
+ /// Устанавливает числовой формат ячейки (не меняя значение).
+ ICell Set(NumberFormatPattern format);
+
+ /// Устанавливает выравнивание текста ячейки.
+ ICell Set(CellAlign format);
+
+ /// Устанавливает границы ячейки.
+ ICell Set(CellBorder format);
+
+ /// Устанавливает заливку ячейки.
+ ICell Set(CellFill format);
+
+ /// Устанавливает шрифт ячейки.
+ ICell Set(CellFont format);
+
+ /// Устанавливает богатый текст (форматированный) с помощью делегата.
+ ICell Set(Action value);
+
+ /// Устанавливает простое текстовое значение (без форматирования).
+ ICell Set(string value);
+
+ /// Устанавливает логическое значение.
+ ICell Set(bool value);
+
+ /// Устанавливает дату, опционально с числовым форматом.
+ ICell Set(DateTime value, NumberFormatPattern? format = null);
+
+ /// Устанавливает десятичное число (сохраняется как double).
+ ICell Set(decimal value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число двойной точности.
+ ICell Set(double value, NumberFormatPattern? format = null);
+
+ /// Устанавливает число с плавающей точкой.
+ ICell Set(float value, NumberFormatPattern? format = null);
+
+ /// Устанавливает целое число.
+ ICell Set(int value, NumberFormatPattern? format = null);
+
+ /// Устанавливает длинное целое число.
+ ICell Set(long value, NumberFormatPattern? format = null);
+
+ /// Очищает содержимое ячейки (значение/формулу, но не форматирование).
+ void ClearContent();
+
+ /// Очищает форматирование ячейки (сбрасывает стиль).
+ void ClearFormat();
+
+ /// Очищает и содержимое, и форматирование.
+ void Clear();
+}
+
+/// Представляет богатый текст внутри ячейки (несколько форматированных фрагментов).
+public interface ICellText
+{
+ /// Количество фрагментов (Run) в тексте.
+ int Count { get; }
+
+ /// Возвращает все фрагменты.
+ IEnumerable GetRuns();
+
+ /// Возвращает фрагмент по индексу или null.
+ IRun? GetRunAt(int index);
+
+ /// Пытается получить фрагмент по индексу.
+ bool TryGetRunAt(int index, out IRun run);
+
+ /// Первый фрагмент или null.
+ IRun? First();
+
+ /// Пытается получить первый фрагмент.
+ bool TryGetFirst(out IRun run);
+
+ /// Последний фрагмент или null.
+ IRun? Last();
+
+ /// Пытается получить последний фрагмент.
+ bool TryGetLast(out IRun run);
+
+ /// Пытается удалить фрагмент.
+ bool TryRemoveRun(IRun run);
+
+ /// Удаляет фрагмент по индексу.
+ bool TryRemoveRun(int index);
+
+ /// Удаляет фрагмент по индексу и возвращает удалённый.
+ bool TryRemoveRun(int index, out IRun? removed);
+
+ /// Добавляет разрыв строки (перенос внутри ячейки).
+ ICellText AddBreak();
+
+ /// Добавляет обычный текстовый фрагмент.
+ ICellText AddRun(string text, RunFormat? format = null);
+
+ /// Добавляет фрагмент с последующим разрывом строки.
+ ICellText AddRunBreak(string text, RunFormat? format = null);
+
+ /// Добавляет подстрочный фрагмент (эквивалентно AddRun с Vertical = Subscript).
+ ICellText AddSubRun(string text, RunFormat? format = null);
+
+ /// Добавляет надстрочный фрагмент.
+ ICellText AddSupRun(string text, RunFormat? format = null);
+
+ /// Вставляет фрагмент по индексу.
+ bool TryInsertRun(int index, string text, RunFormat? format = null);
+
+ /// Вставляет фрагмент с последующим разрывом строки по индексу.
+ bool TryInsertRunBreak(int index, string text, RunFormat? format = null);
+
+ /// Вставляет подстрочный фрагмент по индексу.
+ bool TryInsertSubRun(int index, string text, RunFormat? format = null);
+
+ /// Вставляет надстрочный фрагмент по индексу.
+ bool TryInsertSupRun(int index, string text, RunFormat? format = null);
+
+ /// Применяет заданный формат ко всем существующим фрагментам (поверх их текущего форматирования, заменяя неуказанные свойства).
+ void ApplyFormatToAllRuns(RunFormat format);
+
+ /// Удаляет все фрагменты, очищая текст ячейки.
+ void Clear();
+}
+
+/// Представляет один форматированный фрагмент текста внутри ячейки.
+public interface IRun
+{
+ /// Текст фрагмента.
+ string Text { get; set; }
+
+ /// Форматирование фрагмента (может быть null, что означает отсутствие явного форматирования).
+ RunFormat? Format { get; set; }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs b/QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs
new file mode 100644
index 0000000..7c54fd8
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/NumberFormatPattern.cs
@@ -0,0 +1,20 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+public class NumberFormatPattern
+{
+ public string Format { get; }
+ internal int? Id { get; private set; }
+
+ public NumberFormatPattern(string format, ushort id = 0)
+ {
+ Format = format ?? throw new ArgumentNullException(nameof(format));
+ if (id != 0) Id = id;
+ }
+
+ internal void Attach(ushort id) => Id = id;
+
+ public override bool Equals(object? obj) =>
+ obj is NumberFormatPattern other && Format == other.Format;
+
+ public override int GetHashCode() => Format?.GetHashCode() ?? 0;
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/RowHeight.cs b/QWERTYkez.ExcelProcessor/Editors/RowHeight.cs
new file mode 100644
index 0000000..a7453c0
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/RowHeight.cs
@@ -0,0 +1,29 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Предоставляет типобезопасное представление высоты строки в Excel с поддержкой различных единиц измерения.
+///
+public readonly struct RowHeight(double points)
+{
+ private const double POINTS_PER_INCH = 72.0;
+ private const double INCH_PER_CM = 1.0 / 2.54;
+ private const double POINTS_PER_CM = POINTS_PER_INCH * INCH_PER_CM; // ≈ 28.3464566929
+ private const double POINTS_PER_MM = POINTS_PER_CM / 10.0; // ≈ 2.83464566929
+
+ private const double DXA_PER_POINT = 20.0; // 1 point = 20 dxa
+ private const double POINTS_PER_DXA = 1.0 / DXA_PER_POINT;
+
+ /// Высота в пунктах (points).
+ public double Points { get; } = points;
+
+ public static RowHeight FromPoints(double points) => new(points);
+ public static RowHeight FromInch(double inch) => new(inch * POINTS_PER_INCH);
+ public static RowHeight FromDXA(double dxa) => new(dxa * POINTS_PER_DXA);
+ public static RowHeight FromCM(double cm) => new(cm * POINTS_PER_CM);
+ public static RowHeight FromMM(double mm) => new(mm * POINTS_PER_MM);
+
+ public double Inch => Points / POINTS_PER_INCH;
+ public double DXA => Points / POINTS_PER_DXA;
+ public double CM => Points / POINTS_PER_CM;
+ public double MM => Points / POINTS_PER_MM;
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/Editors/RunFormat.cs b/QWERTYkez.ExcelProcessor/Editors/RunFormat.cs
new file mode 100644
index 0000000..e941fa9
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/Editors/RunFormat.cs
@@ -0,0 +1,70 @@
+namespace QWERTYkez.ExcelProcessor.Editors;
+
+///
+/// Определяет форматирование отдельного фрагмента (Run) внутри ячейки с богатым текстом.
+/// Применяется только к тексту внутри .
+///
+public readonly struct RunFormat
+{
+ /// Жирное начертание фрагмента.
+ public bool? IsBold { get; init; }
+
+ /// Курсив фрагмента.
+ public bool? IsItalic { get; init; }
+
+ /// Стиль подчёркивания (одинарное, двойное, волнистое и т.д.).
+ public UnderlineStyle? Underline { get; init; }
+
+ /// Одинарное зачёркивание.
+ public bool? IsStrike { get; init; }
+
+ /// Цвет текста фрагмента.
+ public ExColor? Color { get; init; }
+
+ /// Размер шрифта фрагмента в пунктах.
+ public double? FontSize { get; init; }
+
+ /// Имя шрифта фрагмента (например, "Calibri").
+ public string? FontFamily { get; init; }
+
+ /// Вертикальное смещение (надстрочный или подстрочный).
+ public VerticalTextRunAlignment? Vertical { get; init; }
+
+ /*
+
+ методы для извлечения OpenXmlElement или других более удобных типов
+
+ public bool TryExtract(out List<...> elements);
+
+ или
+
+ public bool TrySetFor(InlineString str)
+
+ или
+
+ public bool TrySetFor(ExcelRun str)
+
+ */
+}
+
+/// Определяет стиль подчёркивания текста в ячейке или в части текста (Run).
+public enum UnderlineStyle
+{
+ /// Одинарное сплошное подчёркивание.
+ Single,
+ /// Двойное сплошное подчёркивание.
+ Double,
+ /// Одинарное подчёркивание, используемое для бухгалтерских форматов (нижняя граница ячейки).
+ SingleAccounting,
+ /// Двойное подчёркивание для бухгалтерских форматов.
+ DoubleAccounting
+}
+
+/// Вертикальное смещение текста внутри прогона (Run) – надстрочный или подстрочный.
+public enum VerticalTextRunAlignment
+{
+ /// Надстрочный текст (верхний индекс).
+ Superscript,
+ /// Подстрочный текст (нижний индекс).
+ Subscript
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/ExcelProcessor.cs b/QWERTYkez.ExcelProcessor/ExcelProcessor.cs
new file mode 100644
index 0000000..5628bc4
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/ExcelProcessor.cs
@@ -0,0 +1,320 @@
+namespace QWERTYkez.ExcelProcessor;
+
+///
+/// Статический класс для работы с документами Excel через процессоры чтения/записи.
+///
+public static class ExcelProcessor
+{
+ #region Read Operations
+
+ ///
+ /// Пытается открыть документ только для чтения и выполнить действия.
+ ///
+ /// Путь к исходному файлу .xlsx
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryRead(string sourcePath, Action read)
+ {
+ return TryRead(new FileInfo(sourcePath), read);
+ }
+
+ ///
+ /// Пытается открыть документ только для чтения и выполнить действия.
+ ///
+ /// Объект исходного файла .xlsx
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryRead(FileInfo sourceFile, Action read)
+ {
+ if (sourceFile is null || read is null)
+ {
+#if DEBUG
+ Debug.WriteLine("[DEBUG] SourceFile or action is null");
+#endif
+ return false;
+ }
+
+ using var processor = ExcelReader.CreateInternal(sourceFile);
+ if (processor is null)
+ return false;
+
+ try
+ {
+ read(processor);
+ return true;
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Error in read action: {ex.Message}");
+#endif
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Read Operations from Memory
+
+ ///
+ /// Пытается открыть документ из массива байт только для чтения и выполнить действия.
+ ///
+ /// Массив байт, содержащий документ .xlsx
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryRead(byte[] data, Action read)
+ => TryRead(new ReadOnlyMemory(data), read);
+
+ ///
+ /// Пытается открыть документ из только для чтения и выполнить действия.
+ ///
+ /// Буфер с документом .xlsx
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryRead(ReadOnlyMemory data, Action read)
+ {
+ if (data.IsEmpty || read is null)
+ {
+#if DEBUG
+ Debug.WriteLine("[DEBUG] Data is empty or action is null");
+#endif
+ return false;
+ }
+
+ using var processor = ExcelReader.CreateFromData(data);
+ if (processor is null)
+ return false;
+
+ try
+ {
+ read(processor);
+ return true;
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Error in read action from memory: {ex.Message}");
+#endif
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Write Operations
+
+ ///
+ /// Пытается открыть документ для записи и выполнить действия с последующей перезаписью исходного файла.
+ ///
+ /// Путь к исходному файлу .xlsx
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryWrite(string sourcePath, Action write)
+ {
+ return TryWrite(sourcePath, sourcePath, write);
+ }
+
+ ///
+ /// Пытается открыть документ для записи и выполнить действия с последующей перезаписью исходного файла.
+ ///
+ /// Объект исходного файла .xlsx
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryWrite(FileInfo sourceFile, Action write)
+ {
+ return TryWrite(sourceFile, sourceFile.FullName, write);
+ }
+
+ ///
+ /// Пытается открыть документ для записи, выполнить действия и сохранить результат по новому пути.
+ ///
+ /// Путь к исходному файлу .xlsx
+ /// Путь для сохранения результата (null - перезапись исходного файла)
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryWrite(string sourcePath, string? destinationPath, Action write)
+ {
+ return TryWrite(new FileInfo(sourcePath), destinationPath, write);
+ }
+
+ ///
+ /// Пытается открыть документ для записи, выполнить действия и сохранить результат по новому пути.
+ ///
+ /// Объект исходного файла .xlsx
+ /// Путь для сохранения результата (null - перезапись исходного файла)
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryWrite(FileInfo sourceFile, string? destinationPath, Action write)
+ {
+ if (sourceFile is null || write is null)
+ {
+#if DEBUG
+ Debug.WriteLine("[DEBUG] SourceFile or action is null");
+#endif
+ return false;
+ }
+
+ using var processor = ExcelWriter.CreateInternal(sourceFile, destinationPath);
+ if (processor is null)
+ return false;
+
+ try
+ {
+ write(processor);
+ return true;
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Error in write action: {ex.Message}");
+#endif
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Write Operations from Memory
+
+ ///
+ /// Пытается открыть документ из массива байт для записи, выполнить действия и сохранить результат по указанному пути.
+ ///
+ /// Массив байт, содержащий документ .xlsx
+ /// Путь для сохранения результата
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryWrite(byte[] data, string destinationPath, Action write)
+ => TryWrite(new ReadOnlyMemory(data), destinationPath, write);
+
+ ///
+ /// Пытается открыть документ из для записи, выполнить действия и сохранить результат по указанному пути.
+ ///
+ /// Буфер с документом .xlsx
+ /// Путь для сохранения результата
+ /// Действия для выполнения над документом
+ /// если операция выполнена успешно, иначе
+ public static bool TryWrite(ReadOnlyMemory data, string destinationPath, Action write)
+ {
+ if (data.IsEmpty || write is null)
+ {
+#if DEBUG
+ Debug.WriteLine("[DEBUG] Data is empty or action is null");
+#endif
+ return false;
+ }
+
+ using var processor = ExcelWriter.CreateFromData(data, destinationPath);
+ if (processor is null)
+ return false;
+
+ try
+ {
+ write(processor);
+ return true;
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Error in write action from memory: {ex.Message}");
+#endif
+ return false;
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Пытается создать новый документ Excel, отредактировать его и вернуть результат в виде массива байт.
+ ///
+ /// Результирующий массив байт (если операция успешна).
+ /// Действия для заполнения документа.
+ /// если операция выполнена успешно, иначе .
+ public static bool TryCreate(out byte[] result, Action write)
+ {
+ result = null!;
+ if (write is null) return false;
+
+ try
+ {
+ using var writer = ExcelWriter.CreateNew();
+ write(writer);
+ result = writer.GetDocumentBytes(); // добавим этот метод в ExcelWriter
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Пытается создать новый документ Excel, отредактировать его и записать в указанный поток.
+ ///
+ /// Поток, в который будет записан документ.
+ /// Действия для заполнения документа.
+ /// если операция выполнена успешно, иначе .
+ public static bool TryCreate(Stream outputStream, Action write)
+ {
+ if (write is null || outputStream is null) return false;
+
+ try
+ {
+ using var writer = ExcelWriter.CreateNew();
+ write(writer);
+ writer.SaveTo(outputStream);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ ///
+ /// Пытается создать новый документ Excel, отредактировать его и сохранить на диск.
+ ///
+ /// Путь для сохранения результата.
+ /// Действия для заполнения документа.
+ /// если операция выполнена успешно, иначе .
+ public static bool TryCreate(string destinationPath, Action write)
+ {
+ if (string.IsNullOrEmpty(destinationPath) || write is null)
+ return false;
+
+ try
+ {
+ using var writer = ExcelWriter.CreateNew(destinationPath);
+ write(writer);
+ writer.Save();
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Пытается создать новый документ Excel, отредактировать его и прочитать результат в памяти.
+ ///
+ /// Действия для заполнения документа.
+ /// Действия для чтения полученного документа.
+ /// если операция выполнена успешно, иначе .
+ public static bool TryRead(Action write, Action read)
+ {
+ if (write is null || read is null)
+ return false;
+
+ try
+ {
+ using var writer = ExcelWriter.CreateNew();
+ write(writer);
+ using var reader = writer.ToReader();
+ read(reader);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/ExcelReader.cs b/QWERTYkez.ExcelProcessor/ExcelReader.cs
new file mode 100644
index 0000000..a4fa722
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/ExcelReader.cs
@@ -0,0 +1,276 @@
+namespace QWERTYkez.ExcelProcessor;
+
+///
+/// Предоставляет потокобезопасный процессор только для чтения документов Excel (xlsx / xlsm) формата.
+/// Не поддерживает операции изменения документа.
+///
+internal class ExcelReader : IDisposable, IExcelReader
+{
+ internal MemoryStream _ms = null!;
+ internal SpreadsheetDocument _doc = null!;
+ internal bool _disposed;
+ internal readonly object _syncLock = new();
+ internal string? _originalSourcePath;
+
+ internal ExcelReader() { }
+
+ #region Factory Methods
+
+ internal static ExcelReader? CreateInternal(FileInfo sourceFile)
+ {
+ if (sourceFile is null || !sourceFile.Exists)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Source file is null or does not exist: {sourceFile?.FullName}");
+#endif
+ return null;
+ }
+
+ MemoryStream? ms = null;
+ SpreadsheetDocument? doc = null;
+
+ try
+ {
+ ms = new MemoryStream();
+ using (var file = new FileStream(sourceFile.FullName,
+ FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
+ {
+ file.CopyTo(ms);
+ }
+ ms.Position = 0;
+
+ doc = SpreadsheetDocument.Open(ms, isEditable: false,
+ new OpenSettings { AutoSave = false });
+
+ if (doc is not null)
+ {
+ var processor = new ExcelReader
+ {
+ _ms = ms,
+ _doc = doc,
+ _originalSourcePath = sourceFile.FullName,
+ FilePath = sourceFile.FullName
+ };
+
+ return processor;
+ }
+#if DEBUG
+ Debug.WriteLine("[DEBUG] Document body is null or empty");
+#endif
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Error creating read-only processor: {ex.GetType().Name}: {ex.Message}");
+#endif
+ }
+
+ doc?.Dispose();
+ ms?.Dispose();
+ return null;
+ }
+
+ internal static ExcelReader? CreateFromData(ReadOnlyMemory data)
+ {
+ if (data.IsEmpty)
+ return null;
+
+ var ms = new MemoryStream();
+ try
+ {
+ ms.Write(data.ToArray(), 0, data.Length);
+ ms.Position = 0;
+
+ var doc = SpreadsheetDocument.Open(ms, false,
+ new OpenSettings { AutoSave = false });
+
+ if (doc is not null)
+ {
+ var processor = new ExcelReader
+ {
+ _ms = ms,
+ _doc = doc,
+ FilePath = null // из памяти – нет файла
+ };
+
+ return processor;
+ }
+ doc?.Dispose();
+ }
+ catch { }
+
+ ms?.Dispose();
+ return null;
+ }
+
+ #endregion
+
+ #region Writers
+
+ public bool TryWrite(string destinationPath, Action action)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(destinationPath))
+ throw new ArgumentException("Destination path cannot be null or empty", nameof(destinationPath));
+ if (action is null)
+ throw new ArgumentNullException(nameof(action));
+
+ // Копируем данные из текущего потока
+ byte[] data;
+ lock (_syncLock)
+ {
+ _ms.Position = 0;
+ data = _ms.ToArray();
+ }
+
+ using var writable = ExcelWriter.CreateFromData(new ReadOnlyMemory(data), destinationPath);
+ if (writable is null)
+ return false;
+
+ try
+ {
+ action(writable);
+ return true;
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Error in TryWrite action: {ex.Message}");
+#endif
+ return false;
+ }
+ }
+
+ public bool TryWrite(Action write, Action read)
+ {
+ ThrowIfDisposed();
+ if (write is null) throw new ArgumentNullException(nameof(write));
+ if (read is null) throw new ArgumentNullException(nameof(read));
+
+ // Копируем текущие данные
+ byte[] data;
+ lock (_syncLock)
+ {
+ _ms.Position = 0;
+ data = _ms.ToArray();
+ }
+
+ ExcelWriter? writable = null;
+ ExcelReader? resultReader = null;
+ try
+ {
+ // Создаём редактируемую копию (без привязки к файлу)
+ writable = ExcelWriter.CreateFromData(data);
+ if (writable is null)
+ return false;
+
+ // Применяем изменения
+ write(writable);
+
+ // Сохраняем изменения в поток и создаём read-only процессор
+ resultReader = writable.ToReader();
+
+ if (resultReader is null)
+ return false;
+
+ // Работаем с изменённой копией
+ read(resultReader);
+ return true;
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Error in TryWrite(write, read): {ex.Message}");
+#endif
+ return false;
+ }
+ finally
+ {
+ // Гарантированно освобождаем созданные процессоры
+ resultReader?.Dispose();
+ writable?.Dispose();
+ }
+ }
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Получает путь к исходному файлу.
+ ///
+ public string? FilePath { get; protected set; }
+
+ ///
+ /// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций.
+ ///
+ public bool IsValid => !_disposed && _doc is not null && _ms is not null;
+
+ ///
+ /// Получает текущий размер документа в байтах.
+ ///
+ public long DocumentSize => _ms.Length;
+
+ #endregion
+
+ #region Read Operations
+
+ ///
+ /// Находит все уникальные плейсхолдеры в формате $...$ в документе.
+ /// Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов.
+ ///
+ /// Способ сравнения строк при поиске (по умолчанию: без учета регистра)
+ /// Коллекция уникальных найденных плейсхолдеров
+ public ISet FindPlaceholders()
+ {
+ ThrowIfDisposed();
+
+ lock (_syncLock)
+ {
+ return PlaceholderFinder.FindInDocument(_doc);
+ }
+ }
+
+ #endregion
+
+ #region Dispose Pattern
+
+ internal void ThrowIfDisposed()
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(GetType().Name);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ return;
+
+ lock (_syncLock)
+ {
+ if (disposing)
+ {
+ _doc.Dispose();
+ _ms.Dispose();
+ }
+
+ _doc = null!;
+ _ms = null!;
+ _disposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ ~ExcelReader()
+ {
+ Dispose(disposing: false);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/ExcelWriter.cs b/QWERTYkez.ExcelProcessor/ExcelWriter.cs
new file mode 100644
index 0000000..1a1d767
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/ExcelWriter.cs
@@ -0,0 +1,1293 @@
+using QWERTYkez.ExcelProcessor.Editors;
+using System.Runtime.InteropServices;
+
+namespace QWERTYkez.ExcelProcessor;
+
+///
+/// Предоставляет потокобезопасный процессор для чтения и записи документов Excel (xlsx / xlsm) формата.
+/// Наследует от и добавляет операции изменения документа.
+///
+internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
+{
+ // Работа с общей таблицей строк
+ private SharedStringTablePart? _sharedStringPart;
+ private SharedStringTable? _sharedStringTable;
+
+ internal static Dictionary? _calibrationTable; // cw -> width_pts
+
+ internal bool TryGetCalibrateCoeff(double targetPoints, out double result)
+ {
+ result = 0;
+ try
+ {
+ // Используем Interop для точной калибровки
+ _calibrationTable ??= CalibrateWidthCoeffUsingInterop();
+ }
+ catch { }
+
+ _calibrationTable ??= [];
+ if (_calibrationTable.Count < 2)
+ return false;
+
+ // Получаем отсортированные по ключу пары
+ var keys = _calibrationTable.Keys.OrderBy(k => k).ToList();
+ int lastIndex = keys.Count - 1;
+ if (targetPoints <= _calibrationTable[keys[0]])
+ {
+ result = keys[0];
+ return true;
+ }
+ if (targetPoints >= _calibrationTable[keys[lastIndex]])
+ {
+ result = keys[lastIndex];
+ return true;
+ }
+
+ // Бинарный поиск интервала
+ int left = 0, right = keys.Count - 1;
+ while (right - left > 1)
+ {
+ int mid = (left + right) / 2;
+ if (_calibrationTable[keys[mid]] < targetPoints)
+ left = mid;
+ else right = mid;
+ }
+
+ int cwLow = keys[left];
+ int cwHigh = keys[right];
+ double widthLow = _calibrationTable[cwLow];
+ double widthHigh = _calibrationTable[cwHigh];
+
+ // Линейная интерполяция
+ result = cwLow + (targetPoints - widthLow) * (cwHigh - cwLow) / (widthHigh - widthLow);
+ return true;
+ }
+
+ private Dictionary CalibrateWidthCoeffUsingInterop()
+ {
+ object? excelApp = null;
+ object? workbooks = null;
+ object? workbook = null;
+ object? sheets = null;
+ object? worksheet = null;
+ object? columns = null;
+ object? columnA = null;
+
+ try
+ {
+ Type? excelType = Type.GetTypeFromProgID("Excel.Application")
+ ?? throw new InvalidOperationException("Excel не установлен");
+
+ excelApp = Activator.CreateInstance(excelType);
+ dynamic dynamicApp = excelApp;
+ dynamicApp.DisplayAlerts = false;
+ dynamicApp.ScreenUpdating = false;
+
+ workbooks = dynamicApp.Workbooks;
+ workbook = ((dynamic)workbooks).Add();
+
+ // Избегаем скрытых ссылок: сначала получаем коллекцию Sheets, потом элемент
+ sheets = ((dynamic)workbook).Sheets;
+ worksheet = ((dynamic)sheets)[1];
+
+ columns = ((dynamic)worksheet).Columns;
+ columnA = ((dynamic)columns)[1];
+
+ var table = new Dictionary();
+ dynamic dynamicColumnA = columnA;
+
+ for (int cw = 1; cw <= 100; cw++)
+ {
+ dynamicColumnA.ColumnWidth = cw;
+ double widthPt = (double)dynamicColumnA.Width;
+ double widthCm = (double)dynamicApp.PointsToCentimeters(widthPt);
+ table.Add(cw, widthCm);
+ }
+
+ return table;
+ }
+ finally
+ {
+ // Освобождаем строго в обратном порядке создания
+ if (columnA != null) Marshal.ReleaseComObject(columnA);
+ if (columns != null) Marshal.ReleaseComObject(columns);
+ if (worksheet != null) Marshal.ReleaseComObject(worksheet);
+ if (sheets != null) Marshal.ReleaseComObject(sheets);
+
+ if (workbook != null)
+ {
+ try { ((dynamic)workbook).Close(false); } catch { }
+ Marshal.ReleaseComObject(workbook);
+ }
+ if (workbooks != null) Marshal.ReleaseComObject(workbooks);
+
+ if (excelApp != null)
+ {
+ try { ((dynamic)excelApp).Quit(); } catch { }
+ Marshal.ReleaseComObject(excelApp);
+ }
+
+ // Принудительный запуск сборщика мусора для очистки dynamic-оберток
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ }
+
+
+ private void EnsureSharedStringTable()
+ {
+ if (_sharedStringPart != null) return;
+ _sharedStringPart = _doc.WorkbookPart?.GetPartsOfType().FirstOrDefault();
+ if (_sharedStringPart == null)
+ {
+ _sharedStringPart = _doc.WorkbookPart?.AddNewPart();
+ _sharedStringPart!.SharedStringTable = new SharedStringTable();
+ }
+ _sharedStringTable = _sharedStringPart.SharedStringTable;
+ }
+
+ internal string GetSharedString(uint index)
+ {
+ EnsureSharedStringTable();
+ if (_sharedStringTable?.Count?.Value is not { } val || index >= val)
+ return string.Empty;
+ var si = _sharedStringTable.ElementAt((int)index);
+ // Обычный текст
+ var text = si.GetFirstChild()?.Text;
+ if (text != null) return text;
+ // Rich text
+ var sb = new StringBuilder();
+ foreach (var run in si.Elements())
+ {
+ sb.Append(run.GetFirstChild()?.Text);
+ }
+ return sb.ToString();
+ }
+
+ internal int GetOrAddSharedString(string value)
+ {
+ EnsureSharedStringTable();
+ if (_sharedStringTable == null) return -1;
+ // Поиск существующей строки
+ int idx = 0;
+ foreach (var si in _sharedStringTable.Elements())
+ {
+ var text = si.GetFirstChild()?.Text;
+ if (text == value) return idx;
+ idx++;
+ }
+ // Добавление новой
+ var newSi = new SharedStringItem();
+ newSi.Append(new Text(value));
+ _sharedStringTable.Append(newSi);
+ _sharedStringTable.Count = (uint)_sharedStringTable.Elements().Count();
+ return idx;
+ }
+
+ // Определение заблокирована ли ячейка
+ internal bool IsCellLocked(uint styleIndex)
+ {
+ var cellFormat = GetCellFormatAt(styleIndex);
+ if (cellFormat == null) return false;
+ // По умолчанию ячейка заблокирована, если Protection не задан или Locked = true
+ return cellFormat.Protection == null || cellFormat.Protection.Locked == null || cellFormat.Protection.Locked.Value;
+ }
+
+ // Кэши числовых форматов
+ private readonly Dictionary _numberFormatCache = [];
+ private readonly Dictionary _numberFormatIdToPattern = [];
+
+ // Кэши для компонентов стилей (чтобы не создавать дубликаты)
+ private readonly Dictionary _fontCache = [];
+ private readonly Dictionary _fillCache = [];
+ private readonly Dictionary _borderCache = [];
+ private readonly Dictionary _alignmentCache = [];
+
+ // Кэш составных стилей (CellFormat)
+ private readonly Dictionary<(int fontId, int fillId, int borderId, int alignId, int numFmtId), int> _cellFormatCache = [];
+
+ // Конструктор, фабричные методы – без изменений (опущены)
+
+ #region Управление числовыми форматами (расширение IBook)
+
+ public IReadOnlyList GetNumberFormats()
+ {
+ // Возвращаем все пользовательские форматы (Id >= 164), встроенные не включаем
+ var result = new List();
+ var stylesheet = _doc.WorkbookPart?.WorkbookStylesPart?.Stylesheet;
+ if (stylesheet?.NumberingFormats != null)
+ {
+ foreach (var nf in stylesheet.NumberingFormats.Elements())
+ {
+ if (nf.NumberFormatId?.Value >= 164 && nf.FormatCode?.Value is string code)
+ {
+ var pattern = new NumberFormatPattern(code);
+ pattern.Attach((ushort)nf.NumberFormatId.Value);
+ result.Add(pattern);
+ }
+ }
+ }
+ return result;
+ }
+
+ public NumberFormatPattern CreateNumberFormat(string formatCode)
+ {
+ if (string.IsNullOrEmpty(formatCode)) throw new ArgumentException("Format code cannot be empty", nameof(formatCode));
+
+ lock (_syncLock)
+ {
+ return CreateNumberFormatInternal(formatCode);
+ }
+ }
+
+ // Внутренний метод получения NumberFormatPattern по индексу стиля ячейки
+ internal NumberFormatPattern? GetNumberFormat(uint cellStyleIndex)
+ {
+ var cellFormat = GetCellFormatAt(cellStyleIndex);
+ if (cellFormat?.NumberFormatId?.Value is uint numFmtId && cellFormat.ApplyNumberFormat?.Value == true)
+ {
+ if (_numberFormatIdToPattern.TryGetValue(numFmtId, out var pattern))
+ return pattern;
+ // Пытаемся найти в стилях книги
+ var stylesheet = _doc.WorkbookPart?.WorkbookStylesPart?.Stylesheet;
+ if (stylesheet?.NumberingFormats != null)
+ {
+ foreach (var nf in stylesheet.NumberingFormats.Elements())
+ {
+ if (nf.NumberFormatId?.Value == numFmtId && nf.FormatCode?.Value is string code)
+ {
+ pattern = new NumberFormatPattern(code);
+ pattern.Attach((ushort)numFmtId);
+ _numberFormatIdToPattern[numFmtId] = pattern;
+ return pattern;
+ }
+ }
+ }
+ // Встроенный формат
+ if (numFmtId < 164)
+ {
+ // Для встроенных форматов код формата определяем по специальной таблице (не хардкодим!).
+ // Вместо этого возвращаем формат только с Id, без FormatCode.
+ pattern = new NumberFormatPattern(string.Empty);
+ pattern.Attach((ushort)numFmtId);
+ _numberFormatIdToPattern[numFmtId] = pattern;
+ return pattern;
+ }
+ }
+ return null;
+ }
+
+ #endregion
+
+ #region Управление составными стилями (CellFormat)
+
+ // Получить индекс CellFormat для комбинации компонентов
+ internal int GetOrCreateCellFormatId(
+ NumberFormatPattern? numberFormat = null,
+ CellFont? font = null,
+ CellFill? fill = null,
+ CellBorder? border = null,
+ CellAlign? align = null)
+ {
+ lock (_syncLock)
+ {
+ // Получаем или создаём числовой формат ID
+ int numFmtId = -1;
+ if (numberFormat != null)
+ {
+ if (numberFormat.Id.HasValue && numberFormat.Id.Value < 164)
+ numFmtId = numberFormat.Id.Value;
+ else
+ numFmtId = (int)GetOrCreateNumberFormatId(numberFormat);
+ }
+
+ // Получаем или создаём Font, Fill, Border, Alignment
+ int fontId = font.HasValue ? GetOrCreateFontId(font.Value) : -1;
+ int fillId = fill.HasValue ? GetOrCreateFillId(fill.Value) : -1;
+ int borderId = border.HasValue ? GetOrCreateBorderId(border.Value) : -1;
+ int alignId = align.HasValue ? GetOrCreateAlignmentId(align.Value) : -1;
+
+ var key = (fontId, fillId, borderId, alignId, numFmtId);
+ if (_cellFormatCache.TryGetValue(key, out int existingIndex))
+ return existingIndex;
+
+ var stylesheet = EnsureStylesheet();
+ var cellFormats = stylesheet.CellFormats ?? CreateCellFormats(stylesheet);
+
+ var cellFormat = new CellFormat();
+ if (fontId >= 0)
+ {
+ cellFormat.FontId = (uint)fontId;
+ cellFormat.ApplyFont = true;
+ }
+ if (fillId >= 0)
+ {
+ cellFormat.FillId = (uint)fillId;
+ cellFormat.ApplyFill = true;
+ }
+ if (borderId >= 0)
+ {
+ cellFormat.BorderId = (uint)borderId;
+ cellFormat.ApplyBorder = true;
+ }
+ if (alignId >= 0)
+ {
+ cellFormat.Alignment = GetAlignmentFromCache(alignId);
+ cellFormat.ApplyAlignment = true;
+ }
+ if (numFmtId >= 0)
+ {
+ cellFormat.NumberFormatId = (uint)numFmtId;
+ cellFormat.ApplyNumberFormat = true;
+ }
+
+ cellFormats.Append(cellFormat);
+ int newIndex = (int)(cellFormats.Count?.Value ?? 0);
+ cellFormats.Count = (uint)(newIndex + 1);
+ _cellFormatCache[key] = newIndex;
+ return newIndex;
+ }
+ }
+
+ #endregion
+
+ #region Вспомогательные методы для работы со стилями
+
+ private Stylesheet EnsureStylesheet()
+ {
+ var workbookPart = _doc.WorkbookPart ?? throw new InvalidOperationException("No WorkbookPart");
+ var stylesPart = workbookPart.WorkbookStylesPart;
+ if (stylesPart == null)
+ {
+ stylesPart = workbookPart.AddNewPart();
+ stylesPart.Stylesheet = new Stylesheet();
+ }
+ var stylesheet = stylesPart.Stylesheet!;
+ if (stylesheet.Fonts == null)
+ {
+ stylesheet.Fonts = new Fonts();
+ stylesheet.Fonts.Append(new Font()); // минимум один шрифт
+ stylesheet.Fonts.Count = 1;
+ }
+ if (stylesheet.Fills == null)
+ {
+ stylesheet.Fills = new Fills();
+ stylesheet.Fills.Append(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } });
+ stylesheet.Fills.Append(new Fill { PatternFill = new PatternFill { PatternType = PatternValues.Gray125 } });
+ stylesheet.Fills.Count = 2;
+ }
+ if (stylesheet.Borders == null)
+ {
+ stylesheet.Borders = new Borders();
+ stylesheet.Borders.Append(new Border()); // пустая граница
+ stylesheet.Borders.Count = 1;
+ }
+ if (stylesheet.CellFormats == null)
+ {
+ stylesheet.CellFormats = new CellFormats();
+ stylesheet.CellFormats.Append(new CellFormat()); // стандартный стиль
+ stylesheet.CellFormats.Count = 1;
+ }
+ return stylesheet;
+ }
+
+ private uint GetOrCreateNumberFormatId(NumberFormatPattern pattern)
+ {
+ if (pattern.Id.HasValue)
+ return (uint)pattern.Id.Value;
+
+ // Создаём через внутренний метод (без дополнительного lock)
+ var created = CreateNumberFormatInternal(pattern.Format);
+ return (uint)created.Id!.Value;
+ }
+
+ private int GetOrCreateFontId(CellFont font)
+ {
+ if (_fontCache.TryGetValue(font, out int id))
+ return id;
+
+ var stylesheet = EnsureStylesheet();
+ var fonts = stylesheet.Fonts!;
+ var excelFont = font.ToFont() ?? new Font();
+ fonts.Append(excelFont);
+ uint newId = fonts.Count!.Value;
+ fonts.Count = newId + 1u;
+ return _fontCache[font] = (int)newId;
+ }
+
+ private int GetOrCreateFillId(CellFill fill)
+ {
+ if (_fillCache.TryGetValue(fill, out int id))
+ return id;
+
+ var stylesheet = EnsureStylesheet();
+ var fills = stylesheet.Fills!;
+ var excelFill = fill.ToFill() ?? new Fill { PatternFill = new PatternFill { PatternType = PatternValues.None } };
+ fills.Append(excelFill);
+ uint newId = fills.Count!.Value;
+ fills.Count = newId + 1;
+ return _fillCache[fill] = (int)newId;
+ }
+
+ private int GetOrCreateBorderId(CellBorder border)
+ {
+ if (_borderCache.TryGetValue(border, out int id))
+ return id;
+
+ var stylesheet = EnsureStylesheet();
+ var borders = stylesheet.Borders!;
+ var excelBorder = border.ToBorder() ?? new Border();
+ borders.Append(excelBorder);
+ int newId = (int)borders.Count!.Value;
+ borders.Count = (uint)(newId + 1);
+ _borderCache[border] = newId;
+ return newId;
+ }
+
+ private int GetOrCreateAlignmentId(CellAlign align)
+ {
+ if (_alignmentCache.TryGetValue(align, out int id))
+ return id;
+
+ // Выравнивание не хранится отдельно, а встраивается в CellFormat.
+ // Поэтому мы не создаём отдельный элемент, а возвращаем уникальный ID для кэша стилей.
+ // Но для целостности кэша сохраняем ID.
+ id = _alignmentCache.Count;
+ _alignmentCache[align] = id;
+ return id;
+ }
+
+ private Alignment GetAlignmentFromCache(int alignId)
+ {
+ foreach (var pair in _alignmentCache)
+ if (pair.Value == alignId)
+ return pair.Key.ToAlignment() ?? new Alignment();
+ return new Alignment();
+ }
+
+ private CellFormat? GetCellFormatAt(uint index)
+ {
+ var stylesheet = EnsureStylesheet();
+ if (stylesheet.CellFormats == null || index >= stylesheet.CellFormats.Count!.Value)
+ return null;
+ return (CellFormat)stylesheet.CellFormats.ElementAt((int)index);
+ }
+
+ // Вспомогательные создания коллекций
+ private static NumberingFormats CreateNumberingFormats(Stylesheet stylesheet)
+ {
+ var nfs = new NumberingFormats();
+ stylesheet.NumberingFormats = nfs;
+ return nfs;
+ }
+
+ private static CellFormats CreateCellFormats(Stylesheet stylesheet)
+ {
+ var cfs = new CellFormats();
+ stylesheet.CellFormats = cfs;
+ return cfs;
+ }
+
+ #endregion
+
+
+
+ private bool _isModified = false;
+
+ internal ExcelWriter() { }
+
+ #region Factory Methods
+
+ internal static ExcelWriter? CreateFromData(ReadOnlyMemory data, string destinationPath)
+ {
+ if (data.IsEmpty || string.IsNullOrEmpty(destinationPath))
+ return null;
+
+ var ms = new MemoryStream();
+ try
+ {
+ // Копируем данные в MemoryStream
+ ms.Write(data.ToArray(), 0, data.Length);
+ ms.Position = 0;
+
+ var doc = SpreadsheetDocument.Open(ms, true, new OpenSettings { AutoSave = false });
+
+ if (doc is not null)
+ {
+ return new ExcelWriter
+ {
+ _ms = ms,
+ _doc = doc,
+ FilePath = destinationPath,
+ _originalSourcePath = null // нет исходного файла
+ };
+ }
+ doc?.Dispose();
+ }
+ catch { }
+
+ ms?.Dispose();
+ return null;
+ }
+
+ internal static new ExcelWriter? CreateFromData(ReadOnlyMemory data)
+ {
+ if (data.IsEmpty)
+ return null;
+
+ var ms = new MemoryStream();
+ try
+ {
+ ms.Write(data.ToArray(), 0, data.Length);
+ ms.Position = 0;
+
+ var doc = SpreadsheetDocument.Open(ms, true, new OpenSettings { AutoSave = false });
+
+ if (doc is not null)
+ {
+ return new ExcelWriter
+ {
+ _ms = ms,
+ _doc = doc,
+ FilePath = null // из памяти – нет файла
+ };
+ }
+ doc?.Dispose();
+ }
+ catch { }
+
+ ms?.Dispose();
+ return null;
+ }
+
+ internal static ExcelWriter? CreateInternal(FileInfo sourceFile, string? destinationPath = null!)
+ {
+ if (sourceFile is null || !sourceFile.Exists) return null;
+
+ var ms = new MemoryStream();
+ try
+ {
+ using (var file = new FileStream(sourceFile.FullName,
+ FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
+ {
+ file.CopyTo(ms);
+ }
+ ms.Position = 0;
+
+ var doc = SpreadsheetDocument.Open(ms, isEditable: true,
+ new OpenSettings { AutoSave = false });
+
+ if (doc is not null)
+ {
+ return new ExcelWriter
+ {
+ _ms = ms,
+ _doc = doc,
+ _originalSourcePath = sourceFile.FullName,
+ FilePath = destinationPath
+ };
+ }
+ doc?.Dispose();
+ }
+ catch { }
+
+ ms?.Dispose();
+ return null;
+ }
+
+ ///
+ /// Создаёт новый пустой документ Excel с одним листом.
+ ///
+ /// Путь, по которому будет сохранён документ (необязательный).
+ /// Экземпляр для редактирования нового документа.
+ internal static ExcelWriter CreateNew(string? destinationPath = null)
+ {
+ var ms = new MemoryStream();
+ try
+ {
+ var doc = SpreadsheetDocument.Create(ms, SpreadsheetDocumentType.Workbook);
+ var workbookPart = doc.AddWorkbookPart();
+ workbookPart.Workbook = new Workbook();
+
+ var worksheetPart = workbookPart.AddNewPart();
+ worksheetPart.Worksheet = new Worksheet(new SheetData());
+
+ var sheets = workbookPart.Workbook.AppendChild(new Sheets());
+ //sheets.AppendChild(new Sheet()
+ //{
+ // Id = workbookPart.GetIdOfPart(worksheetPart),
+ // SheetId = 1,
+ // Name = "Лист1"
+ //});
+
+ return new ExcelWriter
+ {
+ _ms = ms,
+ _doc = doc,
+ FilePath = destinationPath,
+ _originalSourcePath = null
+ };
+ }
+ catch
+ {
+ ms.Dispose();
+ throw;
+ }
+ }
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Получает значение, указывающее, будет ли документ сохранен с перезаписью исходного файла.
+ ///
+ public bool WillOverwriteSource => FilePath == _originalSourcePath;
+
+ #endregion
+
+ #region Replace
+
+ // string
+
+ void IExcelWriter.Replace(string oldValue, string newValue, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(oldValue))
+ throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
+
+ if (string.IsNullOrEmpty(newValue)) return;
+
+ lock (_syncLock)
+ {
+ _doc.Replace(oldValue, newValue, comparisonType);
+ _isModified = true;
+ }
+ }
+
+ void IExcelWriter.Replace(IDictionary replacements, StringComparison comparisonType) =>
+ ((IExcelWriter)this).Replace((IEnumerable>)replacements, comparisonType);
+ void IExcelWriter.Replace(IEnumerable> replacements, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (replacements is null) return;
+
+ lock (_syncLock)
+ {
+ _doc.Replace(replacements, comparisonType);
+ _isModified = true;
+ }
+ }
+
+
+ // double
+
+ void IExcelWriter.Replace(string oldValue, double newValue, string? format, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(oldValue))
+ throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
+
+ lock (_syncLock)
+ {
+ _doc.Replace(oldValue, newValue, format);
+ _isModified = true;
+ }
+ }
+
+ void IExcelWriter.Replace(IDictionary replacements, string? format, StringComparison comparisonType) =>
+ ((IExcelWriter)this).Replace((IEnumerable>)replacements, format, comparisonType);
+ void IExcelWriter.Replace(IEnumerable> replacements, string? format, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (replacements == null) return;
+
+ lock (_syncLock)
+ {
+ _doc.Replace(replacements, format, comparisonType);
+ _isModified = true;
+ }
+ }
+
+
+ // float
+
+ void IExcelWriter.Replace(string oldValue, float newValue, string? format, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(oldValue))
+ throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
+
+ lock (_syncLock)
+ {
+ _doc.Replace(oldValue, newValue, format);
+ _isModified = true;
+ }
+ }
+
+ void IExcelWriter.Replace(IDictionary replacements, string? format, StringComparison comparisonType) =>
+ ((IExcelWriter)this).Replace((IEnumerable>)replacements, format, comparisonType);
+ void IExcelWriter.Replace(IEnumerable> replacements, string? format, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (replacements == null) return;
+
+ lock (_syncLock)
+ {
+ _doc.Replace(replacements, format, comparisonType);
+ _isModified = true;
+ }
+ }
+
+
+ // int
+
+ void IExcelWriter.Replace(string oldValue, int newValue, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(oldValue))
+ throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
+
+ lock (_syncLock)
+ {
+ _doc.Replace(oldValue, newValue);
+ _isModified = true;
+ }
+ }
+
+ void IExcelWriter.Replace(IDictionary replacements, StringComparison comparisonType) =>
+ ((IExcelWriter)this).Replace((IEnumerable>)replacements, comparisonType);
+ void IExcelWriter.Replace(IEnumerable> replacements, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (replacements == null) return;
+
+ lock (_syncLock)
+ {
+ _doc.Replace(replacements, comparisonType);
+ _isModified = true;
+ }
+ }
+
+
+ // long
+
+ void IExcelWriter.Replace(string oldValue, long newValue, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(oldValue))
+ throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
+
+ lock (_syncLock)
+ {
+ _doc.Replace(oldValue, newValue);
+ _isModified = true;
+ }
+ }
+
+ void IExcelWriter.Replace(IDictionary replacements, StringComparison comparisonType) =>
+ ((IExcelWriter)this).Replace((IEnumerable>)replacements, comparisonType);
+ void IExcelWriter.Replace(IEnumerable> replacements, StringComparison comparisonType)
+ {
+ ThrowIfDisposed();
+
+ if (replacements == null) return;
+
+ lock (_syncLock)
+ {
+ _doc.Replace(replacements, comparisonType);
+ _isModified = true;
+ }
+ }
+
+ #endregion
+
+ #region Save Operations
+
+ ///
+ /// Сохраняет документ в файл, указанный при создании процессора.
+ ///
+ public void Save()
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(FilePath))
+ throw new InvalidOperationException("Cannot save - no file path specified");
+
+ SaveTo(FilePath!);
+ }
+
+ ///
+ /// Сохраняет документ в указанный файл.
+ ///
+ public void SaveTo(string path)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(path))
+ throw new ArgumentException("Path cannot be null or empty", nameof(path));
+
+ lock (_syncLock)
+ {
+ try
+ {
+ if (_ms is not null)
+ {
+ EnsureFullCalculationOnLoad();
+ _doc.Save();
+ _ms.Position = 0;
+
+ using var fileStream = new FileStream(
+ path,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 65536);
+
+ _ms.CopyTo(fileStream);
+ _isModified = false;
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new IOException($"Failed to save document to '{path}'", ex);
+ }
+ }
+ }
+ internal byte[] GetDocumentBytes()
+ {
+ lock (_syncLock)
+ {
+ EnsureFullCalculationOnLoad();
+ _doc.Save();
+ _ms.Position = 0;
+ return _ms.ToArray();
+ }
+ }
+
+ public void SaveTo(Stream outputStream)
+ {
+ lock (_syncLock)
+ {
+ EnsureFullCalculationOnLoad();
+ _doc.Save();
+ _ms.Position = 0;
+ _ms.CopyTo(outputStream);
+ }
+ }
+
+ ///
+ /// Пытается сохранить документ в указанный файл.
+ ///
+ public bool TrySaveTo(string path, out Exception? error)
+ {
+ error = null;
+
+ if (!IsValid || string.IsNullOrEmpty(path))
+ {
+ error = new InvalidOperationException("Processor is not valid or path is empty");
+ return false;
+ }
+
+ try
+ {
+ SaveTo(path);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ error = ex;
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Failed to save to '{path}': {ex.Message}");
+#endif
+ return false;
+ }
+ }
+
+ ///
+ /// Асинхронно сохраняет документ в указанный файл.
+ ///
+ public async Task SaveToAsync(string path, CancellationToken cancellationToken = default)
+ {
+ ThrowIfDisposed();
+
+ if (string.IsNullOrEmpty(path))
+ throw new ArgumentException("Path cannot be null or empty", nameof(path));
+
+ if (_ms is null)
+ throw new InvalidOperationException("Memory stream is not available");
+
+ byte[] buffer;
+ lock (_syncLock)
+ {
+ EnsureFullCalculationOnLoad();
+ _doc.Save();
+ _ms.Position = 0;
+ buffer = _ms.ToArray();
+ }
+
+ using var fileStream = new FileStream(
+ path,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 81920,
+ useAsync: true);
+
+ await fileStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken);
+ }
+
+ /// Создаёт read-only процессор из текущего документа.
+ internal ExcelReader ToReader()
+ {
+ lock (_syncLock)
+ {
+ EnsureFullCalculationOnLoad(); // важно для формул
+ _doc.Save();
+ _ms.Position = 0;
+ var data = _ms.ToArray();
+ return ExcelReader.CreateFromData(data)
+ ?? throw new InvalidOperationException("Failed to create reader from the current document");
+ }
+ }
+
+ #endregion
+
+ #region Dispose Pattern
+
+ protected override void Dispose(bool disposing)
+ {
+ if (_disposed)
+ return;
+
+ lock (_syncLock)
+ {
+ if (disposing)
+ {
+ if (_isModified && !string.IsNullOrEmpty(FilePath))
+ {
+ try
+ {
+ EnsureFullCalculationOnLoad();
+ _doc.Save();
+
+ Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
+
+ _ms.Position = 0;
+ using var fileStream = new FileStream(
+ FilePath!,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 81920);
+
+ _ms.CopyTo(fileStream);
+ }
+ catch (Exception ex)
+ {
+#if DEBUG
+ Debug.WriteLine($"[DEBUG] Auto-save failed during Dispose: {ex.Message}");
+#endif
+ }
+ }
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+
+ #endregion
+
+ private void EnsureFullCalculationOnLoad()
+ {
+ if (_doc?.WorkbookPart?.Workbook == null) return;
+ var workbook = _doc.WorkbookPart.Workbook;
+ var calcProps = workbook.CalculationProperties;
+ if (calcProps == null)
+ {
+ calcProps = new CalculationProperties();
+ workbook.CalculationProperties = calcProps;
+ }
+ calcProps.ForceFullCalculation = true;
+ calcProps.FullCalculationOnLoad = true;
+ }
+
+
+ #region IBook Implementation
+
+ ///
+ public IReadOnlyList GetSheets()
+ {
+ ThrowIfDisposed();
+ lock (_syncLock)
+ {
+ var sheets = new List();
+ var workbookPart = _doc.WorkbookPart;
+ if (workbookPart?.Workbook?.Sheets == null)
+ return sheets;
+
+ foreach (Sheet sheetElement in workbookPart.Workbook.Sheets.Elements())
+ {
+ var sheet = new ExcelSheet(this, sheetElement, sheetElement.SheetId?.Value ?? 0);
+ sheets.Add(sheet);
+ }
+ return sheets;
+ }
+ }
+
+ ///
+ public ISheet? Sheet(string name)
+ {
+ ThrowIfDisposed();
+ if (string.IsNullOrEmpty(name))
+ return null;
+
+ lock (_syncLock)
+ {
+ var workbookPart = _doc.WorkbookPart;
+ if (workbookPart?.Workbook?.Sheets == null)
+ return null;
+
+ foreach (Sheet sheetElement in workbookPart.Workbook.Sheets.Elements())
+ {
+ if (sheetElement.Name?.Value == name)
+ return new ExcelSheet(this, sheetElement, sheetElement.SheetId?.Value ?? 0);
+ }
+ return null;
+ }
+ }
+
+ ///
+ public bool TryGetSheet(string name, out ISheet sheet)
+ {
+ sheet = Sheet(name)!;
+ return sheet != null;
+ }
+
+ ///
+ public bool TryAddSheet(string name, Action? edit)
+ {
+ ThrowIfDisposed();
+ if (string.IsNullOrEmpty(name))
+ return false;
+
+ lock (_syncLock)
+ {
+ var workbookPart = _doc.WorkbookPart;
+ if (workbookPart?.Workbook?.Sheets == null)
+ return false;
+
+ // Проверка уникальности имени
+ foreach (Sheet s in workbookPart.Workbook.Sheets.Elements())
+ {
+ if (s.Name?.Value == name)
+ return false;
+ }
+
+ // Создание нового листа
+ var worksheetPart = workbookPart.AddNewPart();
+ worksheetPart.Worksheet = new Worksheet(new SheetData());
+
+ // Вычисление нового SheetId
+ uint maxSheetId = 0;
+ foreach (Sheet s in workbookPart.Workbook.Sheets.Elements())
+ {
+ if (s.SheetId?.Value > maxSheetId)
+ maxSheetId = s.SheetId.Value;
+ }
+ uint newSheetId = maxSheetId + 1;
+
+ var sheetElement = new Sheet
+ {
+ Id = workbookPart.GetIdOfPart(worksheetPart),
+ SheetId = newSheetId,
+ Name = name
+ };
+ workbookPart.Workbook.Sheets.Append(sheetElement);
+ _isModified = true;
+
+ var newSheet = new ExcelSheet(this, sheetElement, newSheetId);
+ edit?.Invoke(newSheet);
+ return true;
+ }
+ }
+
+ ///
+ public bool TryRemoveSheet(string name)
+ {
+ var sheet = Sheet(name);
+ return sheet != null && TryRemoveSheet(sheet);
+ }
+
+ ///
+ public bool TryRemoveSheet(ISheet sheet)
+ {
+ ThrowIfDisposed();
+ if (sheet is not ExcelSheet xSheet || xSheet.Book != this)
+ return false;
+
+ lock (_syncLock)
+ {
+ var workbookPart = _doc.WorkbookPart;
+ if (workbookPart?.Workbook?.Sheets == null)
+ return false;
+
+ var sheetElement = xSheet.SheetElement;
+ if (sheetElement?.Parent != workbookPart.Workbook.Sheets)
+ return false;
+
+ // Удаление связанной части
+ string partId = sheetElement.Id?.Value!;
+ if (!string.IsNullOrEmpty(partId))
+ {
+ var part = workbookPart.GetPartById(partId);
+ if (part != null)
+ workbookPart.DeletePart(part);
+ }
+
+ sheetElement.Remove();
+ _isModified = true;
+ return true;
+ }
+ }
+
+ #endregion
+
+ #region IExcelWriter
+
+ ///
+ public void Edit(Action edit)
+ {
+ ThrowIfDisposed();
+ if (edit is null)
+ throw new ArgumentNullException(nameof(edit));
+
+ lock (_syncLock)
+ {
+ edit(this);
+ _isModified = true;
+ }
+ }
+
+ #endregion
+
+
+ // Добавить в класс ExcelWriter
+
+ internal CellAlign GetCellAlign(uint styleIndex)
+ {
+ var cellFormat = GetCellFormatAt(styleIndex);
+ return CellAlign.FromAlignment(cellFormat?.Alignment);
+ }
+
+ internal CellBorder GetCellBorder(uint styleIndex)
+ {
+ var cellFormat = GetCellFormatAt(styleIndex);
+ if (cellFormat?.BorderId?.Value is not uint borderId)
+ return default;
+ var borderElement = GetBorderById(borderId);
+ return CellBorder.FromBorder(borderElement);
+ }
+
+ internal CellFill GetCellFill(uint styleIndex)
+ {
+ var cellFormat = GetCellFormatAt(styleIndex);
+ if (cellFormat?.FillId?.Value is not uint fillId)
+ return default;
+ var fill = GetFillById(fillId);
+ return CellFill.FromFill(fill);
+ }
+
+ internal CellFont GetCellFont(uint styleIndex)
+ {
+ var cellFormat = GetCellFormatAt(styleIndex);
+ if (cellFormat?.FontId?.Value is not uint fontId)
+ return default;
+ var font = GetFontById(fontId);
+ return CellFont.FromFont(font);
+ }
+
+
+ // Внутренний метод, предполагает, что _syncLock уже захвачен вызывающим
+ private NumberFormatPattern CreateNumberFormatInternal(string formatCode)
+ {
+ // Проверяем кэш по коду
+ if (_numberFormatCache.TryGetValue(formatCode, out var existing))
+ return existing;
+
+ // Получаем или создаём NumberingFormat в стилях
+ var stylesheet = EnsureStylesheet();
+ var numberingFormats = stylesheet.NumberingFormats ?? CreateNumberingFormats(stylesheet);
+ uint newId = 164;
+ foreach (var nf in numberingFormats.Elements())
+ {
+ if (nf.FormatCode?.Value == formatCode)
+ {
+ var pattern = new NumberFormatPattern(formatCode);
+ pattern.Attach((ushort)nf.NumberFormatId!.Value);
+ _numberFormatCache[formatCode] = pattern;
+ return pattern;
+ }
+ if (nf.NumberFormatId?.Value >= newId)
+ newId = nf.NumberFormatId.Value + 1;
+ }
+
+ var newNf = new NumberingFormat
+ {
+ NumberFormatId = newId,
+ FormatCode = formatCode
+ };
+ numberingFormats.Append(newNf);
+ numberingFormats.Count = (uint)numberingFormats.Elements().Count();
+
+ var result = new NumberFormatPattern(formatCode);
+ result.Attach((ushort)newId);
+ _numberFormatCache[formatCode] = result;
+ return result;
+ }
+
+
+ // Вспомогательные методы для извлечения элементов из стилей (нужно закешировать или обращаться напрямую)
+
+ private Border? GetBorderById(uint borderId)
+ {
+ if (_cachedBorders == null)
+ {
+ var borders = EnsureStylesheet().Borders;
+ _cachedBorders = borders?.Elements().ToList() ?? [];
+ }
+ return borderId < _cachedBorders.Count ? _cachedBorders[(int)borderId] : null;
+ }
+ private List? _cachedBorders;
+
+ private Fill? GetFillById(uint borderId)
+ {
+ if (_cachedFills == null)
+ {
+ var fills = EnsureStylesheet().Fills;
+ _cachedFills = fills?.Elements().ToList() ?? [];
+ }
+ return borderId < _cachedFills.Count ? _cachedFills[(int)borderId] : null;
+ }
+ private List? _cachedFills;
+
+ private Font? GetFontById(uint borderId)
+ {
+ if (_cachedFonts == null)
+ {
+ var borders = EnsureStylesheet().Borders;
+ _cachedFonts = borders?.Elements().ToList() ?? [];
+ }
+ return borderId < _cachedFonts.Count ? _cachedFonts[(int)borderId] : null;
+ }
+ private List? _cachedFonts;
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/IExcelReader.cs b/QWERTYkez.ExcelProcessor/IExcelReader.cs
new file mode 100644
index 0000000..0d10279
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/IExcelReader.cs
@@ -0,0 +1,14 @@
+namespace QWERTYkez.ExcelProcessor;
+
+public interface IExcelReader
+{
+ long DocumentSize { get; }
+ string? FilePath { get; }
+ bool IsValid { get; }
+
+ ISet FindPlaceholders();
+
+
+ bool TryWrite(string destinationPath, Action action);
+ bool TryWrite(Action write, Action read);
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/IExcelWriter.cs b/QWERTYkez.ExcelProcessor/IExcelWriter.cs
new file mode 100644
index 0000000..d599411
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/IExcelWriter.cs
@@ -0,0 +1,29 @@
+using QWERTYkez.ExcelProcessor.Editors;
+
+namespace QWERTYkez.ExcelProcessor;
+
+public interface IExcelWriter : IBook
+{
+ void Save();
+ void SaveTo(string path);
+ Task SaveToAsync(string path, CancellationToken cancellationToken = default);
+ bool TrySaveTo(string path, out Exception? error);
+
+ void Replace(string oldValue, string newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(string oldValue, double newValue, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(string oldValue, float newValue, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(string oldValue, int newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(string oldValue, long newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IDictionary replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IDictionary replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IDictionary replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IDictionary replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IDictionary replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IEnumerable> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IEnumerable> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IEnumerable> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IEnumerable> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+ void Replace(IEnumerable> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
+
+ void Edit(Action edit);
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/NormalizedSet.cs b/QWERTYkez.ExcelProcessor/NormalizedSet.cs
new file mode 100644
index 0000000..d96e8f0
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/NormalizedSet.cs
@@ -0,0 +1,135 @@
+using System.Collections;
+using System.Globalization;
+
+namespace QWERTYkez.ExcelProcessor;
+
+///
+/// Множество строк, которое автоматически приводит все добавляемые элементы
+/// к верхнему регистру и удаляет диакритические знаки (например, 'ё' -> 'Е').
+/// Реализует ISet<string>, поэтому может использоваться там, где ожидается этот интерфейс.
+///
+public class NormalizedSet : ISet
+{
+ private readonly HashSet _inner;
+
+ ///
+ /// Создаёт пустое нормализованное множество.
+ ///
+ public NormalizedSet()
+ {
+ _inner = [];
+ }
+
+ ///
+ /// Создаёт нормализованное множество, заполненное элементами из указанной коллекции.
+ ///
+ /// Коллекция, элементы которой будут нормализованы и добавлены.
+ public NormalizedSet(IEnumerable collection)
+ {
+ _inner = [.. collection.Select(Normalize)];
+ }
+
+ ///
+ /// Нормализует строку: верхний регистр и удаление диакритики.
+ ///
+ private static string Normalize(string s)
+ {
+ if (string.IsNullOrEmpty(s))
+ return s;
+
+ var normalized = s.Normalize(NormalizationForm.FormD);
+ var sb = new StringBuilder();
+ foreach (char c in normalized)
+ {
+ if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)
+ sb.Append(c);
+ }
+ return sb.ToString().Normalize(NormalizationForm.FormC).ToUpperInvariant();
+ }
+
+ // ---------- Реализация ISet ----------
+
+ public bool Add(string item) => _inner.Add(Normalize(item));
+
+ void ICollection.Add(string item) => Add(item);
+
+ public void UnionWith(IEnumerable other)
+ {
+ foreach (var item in other)
+ Add(item);
+ }
+
+ public void IntersectWith(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ _inner.IntersectWith(normalizedOther);
+ }
+
+ public void ExceptWith(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ _inner.ExceptWith(normalizedOther);
+ }
+
+ public void SymmetricExceptWith(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ _inner.SymmetricExceptWith(normalizedOther);
+ }
+
+ public bool IsSubsetOf(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ return _inner.IsSubsetOf(normalizedOther);
+ }
+
+ public bool IsSupersetOf(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ return _inner.IsSupersetOf(normalizedOther);
+ }
+
+ public bool IsProperSupersetOf(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ return _inner.IsProperSupersetOf(normalizedOther);
+ }
+
+ public bool IsProperSubsetOf(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ return _inner.IsProperSubsetOf(normalizedOther);
+ }
+
+ public bool Overlaps(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ return _inner.Overlaps(normalizedOther);
+ }
+
+ public bool SetEquals(IEnumerable other)
+ {
+ var normalizedOther = new HashSet(other.Select(Normalize));
+ return _inner.SetEquals(normalizedOther);
+ }
+
+ // ---------- Реализация ICollection ----------
+
+ public int Count => _inner.Count;
+
+ public bool IsReadOnly => false;
+
+ public void Clear() => _inner.Clear();
+
+ public bool Contains(string item) => _inner.Contains(Normalize(item));
+
+ public void CopyTo(string[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
+
+ public bool Remove(string item) => _inner.Remove(Normalize(item));
+
+ // ---------- Реализация IEnumerable и IEnumerable ----------
+
+ public IEnumerator GetEnumerator() => _inner.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/PlaceholderFinder.cs b/QWERTYkez.ExcelProcessor/PlaceholderFinder.cs
new file mode 100644
index 0000000..f03375a
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/PlaceholderFinder.cs
@@ -0,0 +1,143 @@
+namespace QWERTYkez.ExcelProcessor;
+
+internal static class PlaceholderFinder
+{
+ /// Находит плейсхолдеры $...$ в указанных листах.
+ public static ISet FindInDocument(SpreadsheetDocument doc, params WorksheetPart[] worksheets)
+ {
+ var result = new NormalizedSet();
+ if (doc.WorkbookPart is null) return result;
+
+ var sharedStringTable = doc.WorkbookPart.SharedStringTablePart?.SharedStringTable;
+ foreach (var ws in worksheets)
+ ProcessWorksheet(ws, sharedStringTable, result);
+
+ return result;
+ }
+
+ /// Находит плейсхолдеры во всех листах документа.
+ public static ISet FindInDocument(SpreadsheetDocument doc)
+ {
+ if (doc.WorkbookPart is null) return new NormalizedSet();
+ return FindInDocument(doc, [.. doc.WorkbookPart.WorksheetParts]);
+ }
+
+ private static void ProcessWorksheet(WorksheetPart wsPart, SharedStringTable? sharedStrings, ISet result)
+ {
+ var worksheet = wsPart.Worksheet;
+ if (worksheet is null) return;
+
+ ProcessCells(worksheet, sharedStrings, result);
+ ProcessHeaderFooter(worksheet, result);
+ }
+
+ private static void ProcessCells(Worksheet worksheet, SharedStringTable? sharedStrings, ISet result)
+ {
+ var sheetData = worksheet.GetFirstChild();
+ if (sheetData is null) return;
+
+ List? tempList = null; // буфер для плейсхолдеров из одного текста
+
+ foreach (var row in sheetData.Elements())
+ {
+ foreach (var cell in row.Elements| ())
+ {
+ string text = GetCellValue(cell, sharedStrings);
+ if (string.IsNullOrEmpty(text)) continue;
+
+#if DEBUG
+ if (text.Contains('$'))
+ Debug.WriteLine($"[PlaceholderFinder] Cell {cell.CellReference}: '{text}'");
+#endif
+ if (FindPlaceholdersInText(text, ref tempList))
+ {
+ foreach (var ph in tempList!)
+ result.Add(ph);
+ tempList.Clear();
+ }
+ }
+ }
+ }
+
+ private static void ProcessHeaderFooter(Worksheet worksheet, ISet result)
+ {
+ var hf = worksheet.Descendants().FirstOrDefault();
+ if (hf is null) return;
+
+ var texts = new[]
+ {
+ hf.OddHeader?.Text, hf.OddFooter?.Text,
+ hf.EvenHeader?.Text, hf.EvenFooter?.Text,
+ hf.FirstHeader?.Text, hf.FirstFooter?.Text
+ };
+
+ List? tempList = null;
+ foreach (var text in texts)
+ {
+ if (string.IsNullOrEmpty(text)) continue;
+ if (FindPlaceholdersInText(text!, ref tempList))
+ {
+ foreach (var ph in tempList!)
+ result.Add(ph);
+ tempList.Clear();
+ }
+ }
+ }
+
+ private static string GetCellValue(Cell cell, SharedStringTable? sharedStrings)
+ {
+ if (cell?.CellValue is null && cell?.InlineString is null)
+ return string.Empty;
+
+ // Встроенная строка
+ if (cell.InlineString is not null)
+ return string.Concat(cell.InlineString.Descendants().Select(t => t.Text));
+
+ // Обычное значение или общая строка
+ string raw = cell.CellValue!.InnerText;
+ if (cell.DataType?.Value == CellValues.SharedString)
+ {
+ if (sharedStrings is not null && int.TryParse(raw, out int idx) && idx >= 0 && idx < sharedStrings.Count!)
+ {
+ var si = sharedStrings.ElementAt(idx);
+ return string.Concat(si.Descendants().Select(t => t.Text));
+ }
+ return string.Empty;
+ }
+ return raw;
+ }
+
+ private static unsafe bool FindPlaceholdersInText(string text, ref List? output)
+ {
+ fixed (char* pText = text)
+ {
+ char* start = pText;
+ char* end = pText + text.Length;
+ bool found = false;
+
+ while (start < end)
+ {
+ // Ищем '$'
+ while (start < end && *start != '$') start++;
+ if (start >= end) break;
+ char* open = start++;
+ if (start >= end) break;
+
+ // Ищем закрывающий '$'
+ char* contentStart = start;
+ while (start < end && *start != '$') start++;
+ if (start >= end) break;
+ char* close = start++;
+
+ int len = (int)(close - contentStart);
+ if (len > 0)
+ {
+ found = true;
+ output ??= [];
+ output.Add(new string(contentStart, 0, len));
+ }
+ }
+ return found;
+ }
+ }
+}
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/QWERTYkez.ExcelProcessor.csproj b/QWERTYkez.ExcelProcessor/QWERTYkez.ExcelProcessor.csproj
new file mode 100644
index 0000000..c1d22ee
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/QWERTYkez.ExcelProcessor.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs b/QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs
new file mode 100644
index 0000000..7f500cc
--- /dev/null
+++ b/QWERTYkez.ExcelProcessor/ReplaceNumericExtensions.cs
@@ -0,0 +1,524 @@
+using System.Globalization;
+
+namespace QWERTYkez.ExcelProcessor;
+
+internal static class ReplaceNumericExtensions
+{
+ // =========================== МНОЖЕСТВЕННЫЕ ЗАМЕНЫ ===========================
+
+ // --- Double ---
+ internal static void Replace(this SpreadsheetDocument doc,
+ IEnumerable> replacements, string? format = null,
+ StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+ {
+ if (replacements is null) return;
+ WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
+ if (worksheets.Length < 1) return;
+
+ var coll = replacements as ICollection>;
+ int count = coll?.Count ?? replacements.Count();
+ if (count == 0) return;
+
+ var workbookPart = doc.WorkbookPart!;
+ var stringReplacements = new Dictionary(count);
+ foreach (var kvp in replacements)
+ stringReplacements[kvp.Key] = kvp.Value.ToString(format ?? "G", CultureInfo.CurrentCulture);
+
+ ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, format, comparisonType,
+ (val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture));
+ }
+
+ // --- Float ---
+ internal static void Replace(this SpreadsheetDocument doc,
+ IEnumerable> replacements, string? format = null,
+ StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+ {
+ if (replacements is null) return;
+ WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
+ if (worksheets.Length < 1) return;
+
+ var coll = replacements as ICollection>;
+ int count = coll?.Count ?? replacements.Count();
+ if (count == 0) return;
+
+ var workbookPart = doc.WorkbookPart!;
+ var stringReplacements = new Dictionary(count);
+ foreach (var kvp in replacements)
+ stringReplacements[kvp.Key] = kvp.Value.ToString(format ?? "G", CultureInfo.CurrentCulture);
+
+ ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, format, comparisonType,
+ (val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture));
+ }
+
+ // --- Int ---
+ internal static void Replace(this SpreadsheetDocument doc,
+ IEnumerable> replacements,
+ StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+ {
+ if (replacements is null) return;
+ WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
+ if (worksheets.Length < 1) return;
+
+ var coll = replacements as ICollection>;
+ int count = coll?.Count ?? replacements.Count();
+ if (count == 0) return;
+
+ var workbookPart = doc.WorkbookPart!;
+ var stringReplacements = new Dictionary(count);
+ foreach (var kvp in replacements)
+ stringReplacements[kvp.Key] = kvp.Value.ToString(CultureInfo.CurrentCulture);
+
+ ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, null, comparisonType,
+ (val, _) => val.ToString(CultureInfo.InvariantCulture));
+ }
+
+ // --- Long ---
+ internal static void Replace(this SpreadsheetDocument doc,
+ IEnumerable> replacements,
+ StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
+ {
+ if (replacements is null) return;
+ WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
+ if (worksheets.Length < 1) return;
+
+ var coll = replacements as ICollection | | | | | | | | | | | | |