Добавьте файлы проекта.

This commit is contained in:
melekhin
2026-06-05 15:58:03 +07:00
parent 785bd7dc5d
commit cf8ef7add7
56 changed files with 13478 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -0,0 +1,223 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Определяет выравнивание содержимого ячейки: горизонтальное, вертикальное, перенос текста и уменьшение по размеру.
/// Все свойства опциональны. Если свойство не задано, соответствующий аспект не изменяется.
/// </summary>
public readonly struct CellAlign : IEquatable<CellAlign>
{
/// <summary>Горизонтальное выравнивание.</summary>
public CellAlignHorizontal? Horizontal { get; init; }
/// <summary>Вертикальное выравнивание.</summary>
public CellAlignVertical? Vertical { get; init; }
/// <summary>Переносить ли текст по словам (многострочный режим).</summary>
public bool? WrapText { get; init; }
/// <summary>Уменьшать размер шрифта, чтобы текст поместился в ячейку.</summary>
public bool? ShrinkToFit { get; init; }
/// <summary>Преобразует горизонтальное выравнивание в тип Open XML.</summary>
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;
}
/// <summary>Преобразует вертикальное выравнивание в тип Open XML.</summary>
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;
}
/// <summary>Создаёт элемент Alignment для Open XML.</summary>
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;
}
/// <summary>Создаёт CellAlign из элемента Alignment Open XML.</summary>
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;
}
}
}
/// <summary>Горизонтальное выравнивание содержимого ячейки.</summary>
public enum CellAlignHorizontal
{
/// <summary>По левому краю.</summary>
Left,
/// <summary>По центру.</summary>
Center,
/// <summary>По правому краю.</summary>
Right,
/// <summary>Заполнение (повтор содержимого для заполнения ширины).</summary>
Fill,
/// <summary>По ширине (для многострочного текста).</summary>
Justify,
/// <summary>Центрирование по выделенным ячейкам (визуально, без объединения).</summary>
CenterContinuous,
/// <summary>Распределённый (выравнивание по ширине с пробелами).</summary>
Distributed,
}
/// <summary>Вертикальное выравнивание содержимого ячейки.</summary>
public enum CellAlignVertical
{
/// <summary>По верхнему краю.</summary>
Top,
/// <summary>По центру.</summary>
Center,
/// <summary>По нижнему краю (значение по умолчанию).</summary>
Bottom,
/// <summary>По высоте (для многострочного текста).</summary>
Justify,
/// <summary>Распределённый по вертикали.</summary>
Distributed,
}

View File

@@ -0,0 +1,281 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Определяет границы ячейки: верхнюю, нижнюю, левую, правую и диагональные.
/// Каждая граница может иметь стиль и цвет.
/// </summary>
public readonly struct CellBorder : IEquatable<CellBorder>
{
/// <summary>Верхняя граница.</summary>
public BorderSide? TopBorder { get; init; }
/// <summary>Нижняя граница.</summary>
public BorderSide? BottomBorder { get; init; }
/// <summary>Левая граница.</summary>
public BorderSide? LeftBorder { get; init; }
/// <summary>Правая граница.</summary>
public BorderSide? RightBorder { get; init; }
/// <summary>Диагональная граница «из левого верхнего в правый нижний» (\\).</summary>
public BorderSide? DiagonalLeft { get; init; }
/// <summary>Диагональная граница «из левого нижнего в правый верхний» (//).</summary>
public BorderSide? DiagonalRight { get; init; }
/// <summary>Создаёт элемент Border для Open XML.</summary>
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<TopBorder>();
if (BottomBorder.HasValue)
border.BottomBorder = BottomBorder.Value.ToBorderElement<BottomBorder>();
if (LeftBorder.HasValue)
border.LeftBorder = LeftBorder.Value.ToBorderElement<LeftBorder>();
if (RightBorder.HasValue)
border.RightBorder = RightBorder.Value.ToBorderElement<RightBorder>();
// Обработка диагональных границ
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<DiagonalBorder>();
}
return border;
}
/// <summary>Создаёт CellBorder из элемента Border Open XML.</summary>
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;
}
}
}
/// <summary>Стиль и цвет границы.</summary>
public readonly struct BorderSide : IEquatable<BorderSide>
{
/// <summary>Стиль линии границы.</summary>
public BorderStyle? Style { get; init; }
/// <summary>Цвет границы.</summary>
public ExColor? Color { get; init; }
internal T ToBorderElement<T>() 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;
}
/// <summary>Создаёт BorderSide из элемента границы Open XML (TopBorder, BottomBorder, LeftBorder, RightBorder, DiagonalBorder).</summary>
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;
}
}
}
/// <summary>Тип линии границы</summary>
public enum BorderStyle
{
/// <summary> Тонкая линия </summary>
Thin,
/// <summary> Средняя линия </summary>
Medium,
/// <summary> Штриховая </summary>
Dashed,
/// <summary> Точечная </summary>
Dotted,
/// <summary> Толстая линия </summary>
Thick,
/// <summary> Двойная линия </summary>
Double,
/// <summary> Волосяная (очень тонкая) </summary>
Hair,
/// <summary> Средняя штриховая </summary>
MediumDashed,
/// <summary> Штрих-пунктирная </summary>
DashDot,
/// <summary> Средняя штрих-пунктирная </summary>
MediumDashDot,
/// <summary> Штрих-пунктир-пунктир </summary>
DashDotDot,
/// <summary> Средняя штрих-пунктир-пунктир </summary>
MediumDashDotDot,
/// <summary> Наклонная штрих-пунктирная (для диагональных) </summary>
SlantDashDot,
}

View File

@@ -0,0 +1,53 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Определяет заливку (фон) ячейки.
/// </summary>
public readonly struct CellFill : IEquatable<CellFill>
{
/// <summary>Цвет фона.</summary>
public ExColor? BackgroundColor { get; init; }
/// <summary>Создаёт элемент Fill для Open XML.</summary>
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;
}
/// <summary>Создаёт CellFill из элемента Fill Open XML.</summary>
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;
}

View File

@@ -0,0 +1,112 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Определяет шрифтовое оформление всей ячейки: размер, семейство, цвет, начертание.
/// Все свойства опциональны.
/// </summary>
public readonly struct CellFont : IEquatable<CellFont>
{
/// <summary>Размер шрифта в пунктах (например, 11).</summary>
public double? FontSize { get; init; }
/// <summary>Имя семейства шрифта (например, "Calibri", "Arial").</summary>
public string? FontFamily { get; init; }
/// <summary>Цвет текста.</summary>
public ExColor? FontColor { get; init; }
/// <summary>Жирное начертание.</summary>
public bool? IsBold { get; init; }
/// <summary>Курсив.</summary>
public bool? IsItalic { get; init; }
/// <summary>Подчёркивание (одинарное).</summary>
public bool? IsUnderline { get; init; }
/// <summary>Зачёркивание.</summary>
public bool? IsStrike { get; init; }
/// <summary>Создаёт элемент Font для Open XML.</summary>
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;
}
/// <summary>Создаёт CellFont из элемента Font Open XML.</summary>
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;
}
}
}

View File

@@ -0,0 +1,63 @@
namespace QWERTYkez.ExcelProcessor.Editors
{
/// <summary>
/// Представляет ширину столбца в Excel. Поддерживает задание в символах, пунктах, сантиметрах и миллиметрах.
/// Для методов FromCM/FromMM/FromPoints требуется калибровка через <see cref="ExcelWriter"/>, иначе точность не гарантирована.
/// </summary>
public readonly struct ColumnWidth
{
private readonly double _rawValue;
private readonly UnitType _unit;
private enum UnitType { Characters, Points, Centimeters, Millimeters }
/// <summary>Коэффициент перевода символов в пункты по умолчанию (используется, если нет калибровочной таблицы).</summary>
public static double DefaultPointsPerChar { get; set; } = 5.65;
private ColumnWidth(double value, UnitType unit)
{
_rawValue = value;
_unit = unit;
}
/// <summary>Задаёт ширину в character units (символах стандартного шрифта).</summary>
public static ColumnWidth FromCharacters(double characters) => new(characters, UnitType.Characters);
/// <summary>Задаёт ширину в пунктах.</summary>
[Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
public static ColumnWidth FromPoints(double points) => new(points, UnitType.Points);
/// <summary>Задаёт ширину в сантиметрах.</summary>
[Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
public static ColumnWidth FromCM(double cm) => new(cm, UnitType.Centimeters);
/// <summary>Задаёт ширину в миллиметрах.</summary>
[Obsolete("Для правильной работы требуется установленный Excel, точность гарантирована только для этого компьютера")]
public static ColumnWidth FromMM(double mm) => new(mm, UnitType.Millimeters);
/// <summary>Возвращает целевую ширину в пунктах (если ширина задана в пунктах, см или мм).</summary>
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;
}
}

View File

@@ -0,0 +1,62 @@
namespace QWERTYkez.ExcelProcessor.Editors;
public readonly struct ExColor(System.Drawing.Color? Color)
{
private readonly System.Drawing.Color? color = Color;
/// <summary>Проверяет, является ли цвет автоматическим (т.е. Color == null).</summary>
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;
}
}

View File

@@ -0,0 +1,43 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Внутренняя реализация <see cref="IBook"/>, которая делегирует вызовы к <see cref="ExcelWriter"/>.
/// Используется в ситуациях, когда требуется отдельный объект книги (например, в <see cref="IExcelWriter.Edit"/>).
/// </summary>
internal sealed class ExcelBook : IBook
{
internal readonly ExcelWriter Writer;
/// <summary>
/// Инициализирует обёртку книги.
/// </summary>
/// <param name="writer">Реальный процессор Excel (реализует IBook).</param>
internal ExcelBook(ExcelWriter writer)
{
Writer = writer;
}
/// <inheritdoc />
public IReadOnlyList<ISheet> GetSheets() => Writer.GetSheets();
/// <inheritdoc />
public ISheet? Sheet(string name) => Writer.Sheet(name);
/// <inheritdoc />
public bool TryGetSheet(string name, out ISheet sheet) => Writer.TryGetSheet(name, out sheet);
/// <inheritdoc />
public bool TryAddSheet(string name, Action<ISheet>? edit = null) => Writer.TryAddSheet(name, edit);
/// <inheritdoc />
public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name);
/// <inheritdoc />
public bool TryRemoveSheet(ISheet sheet) => Writer.TryRemoveSheet(sheet);
/// <inheritdoc />
public IReadOnlyList<NumberFormatPattern> GetNumberFormats() => Writer.GetNumberFormats();
/// <inheritdoc />
public NumberFormatPattern CreateNumberFormat(string format) => Writer.CreateNumberFormat(format);
}

View File

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

View File

@@ -0,0 +1,270 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Внутренняя реализация <see cref="ICellText"/> для работы с богатым текстом ячейки.
/// Хранит коллекцию фрагментов <see cref="ExcelRun"/>.
/// Минимизирует аллокации, не использует рефлексию.
/// </summary>
internal sealed class ExcelCellText : ICellText
{
private List<IRun>? _runs;
/// <inheritdoc />
public int Count => _runs?.Count ?? 0;
/// <inheritdoc />
public IEnumerable<IRun> GetRuns()
{
if (_runs is null)
return [];
// Возвращаем сам список, чтобы избежать копирования.
// Вызывающий не должен модифицировать коллекцию.
return _runs;
}
/// <inheritdoc />
public IRun? GetRunAt(int index)
{
if (_runs is null || index < 0 || index >= _runs.Count)
return null;
return _runs[index];
}
/// <inheritdoc />
public bool TryGetRunAt(int index, out IRun run)
{
run = GetRunAt(index)!;
return run != null;
}
/// <inheritdoc />
public IRun? First()
{
if (_runs is null || _runs.Count == 0)
return null;
return _runs[0];
}
/// <inheritdoc />
public bool TryGetFirst(out IRun run)
{
run = First()!;
return run != null;
}
/// <inheritdoc />
public IRun? Last()
{
if (_runs is null || _runs.Count == 0)
return null;
return _runs[_runs.Count - 1];
}
/// <inheritdoc />
public bool TryGetLast(out IRun run)
{
run = Last()!;
return run != null;
}
/// <inheritdoc />
public bool TryRemoveRun(IRun run)
{
if (run is null || _runs is null)
return false;
return _runs.Remove(run);
}
/// <inheritdoc />
public bool TryRemoveRun(int index)
{
if (_runs is null || index < 0 || index >= _runs.Count)
return false;
_runs.RemoveAt(index);
return true;
}
/// <inheritdoc />
public bool TryRemoveRun(int index, out IRun? removed)
{
removed = GetRunAt(index);
if (removed is null)
return false;
return TryRemoveRun(index);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
public ICellText AddRunBreak(string text, RunFormat? format = null)
{
AddRun(text, format);
AddBreak();
return this;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
}
}
/// <inheritdoc />
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
};
}
}

View File

@@ -0,0 +1,502 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Внутренняя реализация <see cref="IColumn"/>.
/// </summary>
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);
/// <summary>Устанавливает ширину столбца без создания объекта IColumn.</summary>
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;
}
}
/// <summary>Возвращает ширину столбца.</summary>
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<Columns>();
if (cols == null) return null;
foreach (Column col in cols.Elements<Column>())
{
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<Columns>();
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<Row>())
{
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<ICell> 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<Row>())
{
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<Columns>();
if (cols == null) return null;
foreach (Column col in cols.Elements<Column>())
{
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<Columns>();
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<Row>())
{
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<Row>().ToList())
{
var cell = FindCellInRow(row, col);
cell?.Remove();
}
}
private Cell? FindCellInRow(Row row, uint colIndex)
{
foreach (var cell in row.Elements<Cell>())
{
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<Cell>().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<Columns>();
if (cols == null) return null;
foreach (Column c in cols.Elements<Column>())
{
if (c.Min?.Value is { } min
&& c.Max?.Value is { } max
&& min <= col
&& max >= col)
return c;
}
return null;
}
private Column GetOrCreateColumnElementForIndex(uint col)
{
var existing = GetColumnElementForIndex(col);
if (existing != null) return existing;
var worksheet = _sheet.Worksheet;
var cols = worksheet.GetFirstChild<Columns>();
if (cols == null)
{
cols = new Columns();
worksheet.InsertAt(cols, 0);
}
var newCol = new Column
{
Min = col,
Max = col,
Width = 8.43,
CustomWidth = true
};
cols.Append(newCol);
return newCol;
}
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;
}
}

View File

@@ -0,0 +1,886 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Внутренняя реализация <see cref="IRange"/>.
/// </summary>
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<MergeCell>())
{
if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var refRange))
{
if (refRange._rowStart == _rowStart && refRange._rowEnd == _rowEnd &&
refRange._colStart == _colStart && refRange._colEnd == _colEnd)
return true;
}
}
return false;
}
}
}
/// <summary>
/// Перемещает текущий диапазон в новую позицию (как "вырезать-вставить").
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
/// </summary>
/// <param name="newRow">Номер строки для нового верхнего левого угла.</param>
/// <param name="newCol">Номер столбца для нового верхнего левого угла.</param>
/// <returns>Тот же объект IRange с новыми координатами.</returns>
public IRange MoveTo(uint newRow, uint newCol)
{
if (newRow == _rowStart && newCol == _colStart) return this;
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
uint newRowEnd = newRow + (_rowEnd - _rowStart);
uint newColEnd = newCol + (_colEnd - _colStart);
bool overlap = RangesOverlap(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, newRowEnd, newColEnd);
bool horizontalShift = (newRow == _rowStart);
bool verticalShift = (newCol == _colStart);
if (!overlap)
{
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, CopyOrder.Any);
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
}
else if (horizontalShift)
{
CopyOrder order = (newCol > _colStart) ? CopyOrder.RightToLeft : CopyOrder.LeftToRight;
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, order);
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
}
else if (verticalShift)
{
CopyOrder order = (newRow > _rowStart) ? CopyOrder.BottomToTop : CopyOrder.TopToBottom;
CopyCells(_rowStart, _colStart, _rowEnd, _colEnd, newRow, newCol, order);
ClearRangeData(_rowStart, _colStart, _rowEnd, _colEnd);
}
else
{
// Диагональное перемещение два последовательных сдвига
MoveTo(_rowStart, newCol);
MoveTo(newRow, _colStart);
return this;
}
_rowStart = newRow;
_rowEnd = newRowEnd;
_colStart = newCol;
_colEnd = newColEnd;
return this;
}
}
public IRange MoveTo(uint rowIndex, string colIndex)
{
return MoveTo(rowIndex, CellAddressHelper.ColumnLetterToIndex(colIndex));
}
private enum CopyOrder
{
Any,
LeftToRight,
RightToLeft,
TopToBottom,
BottomToTop
}
/// <summary>Копирует ячейки из исходного диапазона в целевой, поддерживая различные порядки обхода.</summary>
private void CopyCells(uint srcRowStart, uint srcColStart, uint srcRowEnd, uint srcColEnd,
uint dstRowStart, uint dstColStart, CopyOrder order)
{
// Определяем все строки, которые могут понадобиться (исходные и целевые)
var rowIndices = new HashSet<uint>();
for (uint r = srcRowStart; r <= srcRowEnd; r++) rowIndices.Add(r);
uint dstRowEnd = dstRowStart + (srcRowEnd - srcRowStart);
for (uint r = dstRowStart; r <= dstRowEnd; r++) rowIndices.Add(r);
var rowsDict = GetRowDictionary(rowIndices);
int rowStep = 1, colStep = 1;
int rowStart = (int)srcRowStart, rowEnd = (int)srcRowEnd;
int colStart = (int)srcColStart, colEnd = (int)srcColEnd;
if (order == CopyOrder.BottomToTop)
{
rowStep = -1;
rowStart = (int)srcRowEnd;
rowEnd = (int)srcRowStart;
}
if (order == CopyOrder.RightToLeft)
{
colStep = -1;
colStart = (int)srcColEnd;
colEnd = (int)srcColStart;
}
for (int r = rowStart; (rowStep > 0 ? r <= rowEnd : r >= rowEnd); r += rowStep)
{
if (!rowsDict.TryGetValue((uint)r, out var srcRow))
continue;
for (int c = colStart; (colStep > 0 ? c <= colEnd : c >= colEnd); c += colStep)
{
Cell? srcCell = FindCellInRowFast(srcRow, (uint)c);
if (srcCell == null) continue;
Cell cloned = (Cell)srcCell.CloneNode(true);
uint dstRow = dstRowStart + (uint)(r - srcRowStart);
uint dstCol = dstColStart + (uint)(c - srcColStart);
cloned.CellReference = $"{CellAddressHelper.ColumnIndexToLetter(dstCol)}{dstRow}";
InsertCellAt(dstRow, dstCol, cloned);
}
}
}
/// <summary>Строит словарь строк для указанных индексов строк.</summary>
private Dictionary<uint, Row> GetRowDictionary(HashSet<uint> rowIndices)
{
var dict = new Dictionary<uint, Row>();
var sheetData = _sheet.GetSheetData();
foreach (var row in sheetData.Elements<Row>())
{
if (row.RowIndex?.Value is uint idx && rowIndices.Contains(idx))
dict[idx] = row;
}
return dict;
}
/// <summary>Быстрый поиск ячейки в строке (линейный, подходит для типичного количества ячеек в строке).</summary>
private Cell? FindCellInRowFast(Row row, uint colIndex)
{
foreach (var cell in row.Elements<Cell>())
{
if (CellAddressHelper.TryParseCellReference(cell.CellReference?.Value ?? string.Empty, out _, out uint col) && col == colIndex)
return cell;
}
return null;
}
/// <summary>Проверяет, пересекаются ли два прямоугольных диапазона.</summary>
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);
}
/// <summary>Вставляет ячейку в указанную позицию, предварительно удаляя существующую.</summary>
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);
}
/// <summary>Удаляет ячейку, если она существует.</summary>
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();
}
/// <summary>Получает или создаёт строку с указанным индексом.</summary>
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;
}
/// <summary>Вставляет ячейку в строку с сохранением порядка столбцов.</summary>
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<Cell>().ToList())
{
if (CellAddressHelper.TryParseCellReference(existing.CellReference?.Value ?? string.Empty, out _, out uint existingCol) &&
existingCol > colIndex)
{
existing.InsertBeforeSelf(cell);
inserted = true;
break;
}
}
if (!inserted) row.Append(cell);
}
/// <summary>Очищает данные (содержимое) в указанном диапазоне.</summary>
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<MergeCells>();
if (mergeCells == null)
{
mergeCells = new MergeCells();
worksheet.Append(mergeCells);
}
string refStr = $"{ColStartLetter}{RowStart}:{ColEndLetter}{RowEnd}";
mergeCells.Append(new MergeCell { Reference = refStr });
return true;
}
}
public void Unmerge()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var mergeCells = GetMergeCells();
if (mergeCells == null) return;
var toRemove = mergeCells.Elements<MergeCell>()
.FirstOrDefault(mc => TryParseRangeReference(mc.Reference?.Value ?? string.Empty, out var rng) &&
rng._rowStart == _rowStart && rng._rowEnd == _rowEnd &&
rng._colStart == _colStart && rng._colEnd == _colEnd);
toRemove?.Remove();
}
}
public IRange? GetMergedRange()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var mergeCells = GetMergeCells();
if (mergeCells == null) return null;
foreach (var mergeCell in mergeCells.Elements<MergeCell>())
{
if (TryParseRangeReference(mergeCell.Reference?.Value ?? string.Empty, out var refRange))
{
if (refRange._rowStart >= _rowStart && refRange._rowEnd <= _rowEnd &&
refRange._colStart >= _colStart && refRange._colEnd <= _colEnd)
return refRange;
}
}
return null;
}
}
public bool TryGetMergedRange(IRange range)
{
var merged = GetMergedRange();
if (merged == null) return false;
return merged.Equals(range);
}
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;
}
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
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;
}
}
/// <summary>
/// Применяет стиль ко всем ячейкам диапазона.
/// </summary>
/// <param name="styleIndex">Индекс стиля для применения.</param>
/// <param name="createIfMissing">Если true, создаёт недостающие ячейки (по умолчанию true).</param>
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<ICell> Cells
{
get
{
for (uint r = _rowStart; r <= _rowEnd; r++)
for (uint c = _colStart; c <= _colEnd; c++)
yield return new ExcelCell(_writer, _sheet, r, c);
}
}
public bool GetSubCell(uint row, uint col, out ICell cell)
{
uint globalRow = _rowStart + row - 1;
uint globalCol = _colStart + col - 1;
if (globalRow > _rowEnd || globalCol > _colEnd)
{
cell = default!;
return false;
}
cell = new ExcelCell(_writer, _sheet, globalRow, globalCol);
return true;
}
public bool GetSubCell(uint row, string col, out ICell cell)
{
uint colIndex = CellAddressHelper.ColumnLetterToIndex(col);
return GetSubCell(row, colIndex, out cell);
}
public bool TryEditSubCell(uint row, uint col, Action<ICell> edit)
{
if (GetSubCell(row, col, out var cell))
{
edit(cell);
return true;
}
return false;
}
public bool TryEditSubCell(uint row, uint col, string value)
{
if (GetSubCell(row, col, out var cell))
{
cell.Set(value);
return true;
}
return false;
}
public bool TryEditSubCell(uint row, uint col, bool value)
{
if (GetSubCell(row, col, out var cell))
{
cell.Set(value);
return true;
}
return false;
}
public bool TryEditSubCell(uint row, uint col, string formula, NumberFormatPattern? format = null)
{
if (GetSubCell(row, col, out var cell))
{
cell.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<ICell> edit)
{
if (GetSubCell(row, col, out var cell))
{
edit(cell);
return true;
}
return false;
}
public bool TryEditSubCell(uint row, string col, string value)
{
if (GetSubCell(row, col, out var cell))
{
cell.Set(value);
return true;
}
return false;
}
public bool TryEditSubCell(uint row, string col, bool value)
{
if (GetSubCell(row, col, out var cell))
{
cell.Set(value);
return true;
}
return false;
}
public bool TryEditSubCell(uint row, string col, string formula, NumberFormatPattern? format = null)
{
if (GetSubCell(row, col, out var cell))
{
cell.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<Row>())
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<Row>().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<Cell>())
{
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<Columns>();
if (cols == null) return null;
foreach (Column c in cols.Elements<Column>())
if (c.Min?.Value is { } min
&& c.Max?.Value is { } max
&& min <= col
&& max >= col)
return c;
return null;
}
private Column GetOrCreateColumnElementForIndex(uint col)
{
var existing = GetColumnElementForIndex(col);
if (existing != null) return existing;
var worksheet = _sheet.Worksheet;
var cols = worksheet.GetFirstChild<Columns>();
if (cols == null)
{
cols = new Columns();
worksheet.InsertAt(cols, 0);
}
var newCol = new Column
{
Min = col,
Max = col,
Width = 8.43,
CustomWidth = true
};
cols.Append(newCol);
return newCol;
}
private MergeCells? GetMergeCells() =>
_sheet.Worksheet.GetFirstChild<MergeCells>();
private bool TryParseRangeReference(string reference, out ExcelRange range)
{
range = null!;
if (string.IsNullOrEmpty(reference)) return false;
string[] parts = reference.Split(':');
if (parts.Length != 2) return false;
if (!CellAddressHelper.TryParseCellReference(parts[0] + "1", out uint row1, out uint col1)) return false;
if (!CellAddressHelper.TryParseCellReference(parts[1] + "1", out uint row2, out uint col2)) return false;
uint rowStart = Math.Min(row1, row2), rowEnd = Math.Max(row1, row2);
uint colStart = Math.Min(col1, col2), colEnd = Math.Max(col1, col2);
range = new ExcelRange(_writer, _sheet, rowStart, colStart, rowEnd, colEnd);
return true;
}
// создаёт стиль только с числовым форматом
private int GetOrCreateNumberFormatId(NumberFormatPattern format)
{
return _writer.GetOrCreateCellFormatId(numberFormat: format);
}
}

View File

@@ -0,0 +1,393 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Внутренняя реализация <see cref="IRow"/>.
/// </summary>
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;
}
/// <inheritdoc />
public uint Index => _rowIndex;
/// <summary>Устанавливает высоту строки без создания объекта IRow.</summary>
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;
}
}
/// <summary>Возвращает высоту строки.</summary>
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<Row>())
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<Row>().ToList())
{
if (existing.RowIndex?.Value > rowIndex)
{
existing.InsertBeforeSelf(row);
inserted = true;
break;
}
}
if (!inserted) sheetData.Append(row);
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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;
}
}
}
/// <inheritdoc />
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>())
cell.StyleIndex = (uint)formatIndex;
}
return this;
}
/// <inheritdoc />
public ICell Cell(uint col) => new ExcelCell(_writer, _sheet, _rowIndex, col);
/// <inheritdoc />
public IRow Cell(uint col, Action<ICell> 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<ICell> 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;
}
/// <inheritdoc />
public ICell Cell(string col) => Cell(CellAddressHelper.ColumnLetterToIndex(col));
/// <inheritdoc />
public IRow EditCell(string col, Action<ICell> edit)
{
edit(Cell(col)); return this;
}
/// <inheritdoc />
public void ClearContents()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var rowElement = FindRowElement(_sheet.GetSheetData(), _rowIndex);
if (rowElement == null) return;
foreach (var cell in rowElement.Elements<Cell>().ToList())
{
cell.Remove();
}
}
}
/// <inheritdoc />
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>())
{
cell.StyleIndex = null;
}
}
}
/// <inheritdoc />
public void Clear()
{
ClearContents();
ClearFormats();
}
/// <inheritdoc />
public void Remove()
{
_writer.ThrowIfDisposed();
lock (_writer._syncLock)
{
var sheetData = _sheet.GetSheetData();
var rowElement = FindRowElement(sheetData, _rowIndex);
rowElement?.Remove();
// Сдвиг индексов последующих строк вниз в Open XML не требуется, т.к. они имеют свои RowIndex.
// Но при последующем доступе по индексам нужно будет корректировать.
}
}
/// <inheritdoc />
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<Row>())
{
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<Row>().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);
}
}

View File

@@ -0,0 +1,25 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Внутренняя реализация <see cref="IRun"/> для хранения форматированного фрагмента текста.
/// Минимизирует аллокации, не использует рефлексию, все поля доступны напрямую внутри сборки.
/// </summary>
internal sealed class ExcelRun : IRun
{
private string _text = string.Empty;
private RunFormat? _format;
/// <inheritdoc />
public string Text
{
get => _text;
set => _text = value ?? string.Empty;
}
/// <inheritdoc />
public RunFormat? Format
{
get => _format;
set => _format = value;
}
}

View File

@@ -0,0 +1,277 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Внутренняя реализация <see cref="ISheet"/>.
/// </summary>
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<IRow> edit)
{
edit(Row(row));
return this;
}
public IColumn Col(uint col) => new ExcelColumn(Book, this, col);
public ISheet Col(uint col, Action<IColumn> edit)
{
edit(Col(col));
return this;
}
public IColumn Col(string col) => Col(CellAddressHelper.ColumnLetterToIndex(col));
public ISheet Col(string col, Action<IColumn> 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<ICell> 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<ICell> 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<Row>().ToList())
{
foreach (var cell in row.Elements<Cell>().ToList())
cell.Remove();
}
}
public void ClearFormats()
{
var sheetData = GetSheetData();
foreach (var row in sheetData.Elements<Row>())
{
foreach (var cell in row.Elements<Cell>())
cell.StyleIndex = null;
}
}
public void Clear()
{
ClearContents();
ClearFormats();
}
public void Remove() => Book.TryRemoveSheet(this);
// Вспомогательные методы
internal SheetData GetSheetData()
{
var worksheet = Worksheet;
var sheetData = worksheet.GetFirstChild<SheetData>();
if (sheetData == null)
{
sheetData = new SheetData();
worksheet.Append(sheetData);
}
return sheetData;
}
#region Range Operations
/// <inheritdoc />
public IRange RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol)
{
return new ExcelRange(Book, this, startRow, startCol, endRow, endCol);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public ISheet RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol, Action<IRange> edit)
{
var range = RangeByIndexes(startRow, startCol, endRow, endCol);
edit(range);
return this;
}
/// <inheritdoc />
public ISheet RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol, Action<IRange> edit)
{
var range = RangeByIndexes(startRow, startCol, endRow, endCol);
edit(range);
return this;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public IRange RangeByLength(uint startRow, string startCol, uint rows, uint cols)
{
uint startColIdx = CellAddressHelper.ColumnLetterToIndex(startCol);
return RangeByLength(startRow, startColIdx, rows, cols);
}
/// <inheritdoc />
public ISheet RangeByLength(uint startRow, uint startCol, uint rows, uint cols, Action<IRange> edit)
{
var range = RangeByLength(startRow, startCol, rows, cols);
edit(range);
return this;
}
/// <inheritdoc />
public ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action<IRange> edit)
{
var range = RangeByLength(startRow, startCol, rows, cols);
edit(range);
return this;
}
#endregion
}

View File

@@ -0,0 +1,817 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>Представляет книгу Excel</summary>
public interface IBook
{
/// <summary>Возвращает список всех листов в книге.</summary>
IReadOnlyList<ISheet> GetSheets();
/// <summary>Возвращает лист по имени или null, если лист не найден.</summary>
/// <param name="name">Имя листа (регистр учитывается).</param>
ISheet? Sheet(string name);
/// <summary>Пытается получить лист по имени.</summary>
/// <param name="name">Имя листа.</param>
/// <param name="sheet">Найденный лист или null.</param>
/// <returns>true, если лист найден, иначе false.</returns>
bool TryGetSheet(string name, out ISheet sheet);
/// <summary>Пытается добавить новый лист с указанным именем и выполняет его настройку.</summary>
/// <param name="name">Имя нового листа (должно быть уникальным в книге).</param>
/// <param name="edit">Действие для редактирования листа (может быть null).</param>
/// <returns>true, если лист успешно создан, иначе false (например, имя уже существует).</returns>
bool TryAddSheet(string name, Action<ISheet>? edit = null);
/// <summary>Удаляет лист по имени.</summary>
/// <param name="name">Имя удаляемого листа.</param>
/// <returns>true, если лист существовал и был удалён, иначе false.</returns>
bool TryRemoveSheet(string name);
/// <summary>Удаляет лист.</summary>
/// <param name="sheet">Удаляемый лист.</param>
/// <returns>true, если лист был удалён, иначе false.</returns>
bool TryRemoveSheet(ISheet sheet);
/// <summary>Возвращает список всех числовых форматов, определённых в книге (встроенные и пользовательские).</summary>
IReadOnlyList<NumberFormatPattern> GetNumberFormats();
/// <summary>Создаёт новый пользовательский числовой формат (если формат с таким кодом уже существует, может вернуть существующий).</summary>
/// <param name="format">Код формата (например, "# ##0,00").</param>
/// <returns>Объект формата, привязанный к книге.</returns>
NumberFormatPattern CreateNumberFormat(string format);
}
/// <summary>Представляет лист Excel</summary>
public interface ISheet
{
/// <summary>Индекс листа в книге (начиная с 1).</summary>
int Index { get; }
/// <summary>Имя листа (уникальное в книге).</summary>
string Name { get; }
/// <summary>Пытается изменить имя листа.</summary>
/// <param name="name">Новое имя (не должно совпадать с существующими).</param>
/// <returns>true, если переименование выполнено, иначе false.</returns>
bool TrySetName(string name);
/// <summary>Возвращает строку по её индексу (начиная с 1). Если строка отсутствует, создаёт пустую.</summary>
IRow Row(uint row);
/// <summary>Редактирует строку, применяя делегат, и возвращает текущий лист (fluent).</summary>
/// <param name="row">Индекс строки (начиная с 1).</param>
/// <param name="edit">Действие над строкой.</param>
ISheet Row(uint row, Action<IRow> edit);
/// <summary>Возвращает столбец по его индексу (начиная с 1). Если столбец отсутствует, создаёт пустой.</summary>
IColumn Col(uint col);
/// <summary>Редактирует столбец по индексу.</summary>
ISheet Col(uint col, Action<IColumn> edit);
/// <summary>Возвращает столбец по буквенному обозначению (например, "A", "AB").</summary>
IColumn Col(string col);
/// <summary>Редактирует столбец по буквенному обозначению.</summary>
ISheet Col(string col, Action<IColumn> edit);
/// <summary>Возвращает диапазон ячеек по начальным и конечным индексам строк и столбцов.</summary>
IRange RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol);
/// <summary>Возвращает диапазон ячеек по начальным и конечным координатам с буквенным обозначением столбцов.</summary>
IRange RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol);
/// <summary>Редактирует диапазон, заданный начальными и конечными индексами.</summary>
ISheet RangeByIndexes(uint startRow, uint startCol, uint endRow, uint endCol, Action<IRange> edit);
/// <summary>Редактирует диапазон, заданный начальными и конечными координатами с буквенными столбцами.</summary>
ISheet RangeByIndexes(uint startRow, string startCol, uint endRow, string endCol, Action<IRange> edit);
/// <summary>Возвращает диапазон, начиная с указанной ячейки, заданной размером (строки x столбцы).</summary>
IRange RangeByLength(uint startRow, uint startCol, uint rows, uint cols);
/// <summary>Возвращает диапазон по начальной ячейке и размеру с буквенным обозначением столбца.</summary>
IRange RangeByLength(uint startRow, string startCol, uint rows, uint cols);
/// <summary>Редактирует диапазон, заданный начальной ячейкой и размером.</summary>
ISheet RangeByLength(uint startRow, uint startCol, uint rows, uint cols, Action<IRange> edit);
/// <summary>Редактирует диапазон, заданный начальной ячейкой и размером (буква столбца).</summary>
ISheet RangeByLength(uint startRow, string startCol, uint rows, uint cols, Action<IRange> edit);
/// <summary>Возвращает ячейку по номеру строки и столбца (оба начиная с 1).</summary>
ICell Cell(uint row, uint col);
/// <summary>Редактирует ячейку по строке и столбцу.</summary>
ISheet Cell(uint row, uint col, Action<ICell> edit);
/// <summary>Устанавливает строковое значение в ячейку.</summary>
ISheet Cell(uint row, uint col, string value);
/// <summary>Устанавливает логическое значение в ячейку.</summary>
ISheet Cell(uint row, uint col, bool value);
/// <summary>Устанавливает формулу в ячейку с указанным числовым форматом.</summary>
ISheet Cell(uint row, uint col, string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает дату в ячейку с указанным числовым форматом.</summary>
ISheet Cell(uint row, uint col, DateTime value, NumberFormatPattern? format = null);
/// <summary>Устанавливает десятичное число в ячейку с указанным числовым форматом.</summary>
ISheet Cell(uint row, uint col, decimal value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число двойной точности в ячейку с указанным числовым форматом.</summary>
ISheet Cell(uint row, uint col, double value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число с плавающей точкой в ячейку с указанным числовым форматом.</summary>
ISheet Cell(uint row, uint col, float value, NumberFormatPattern? format = null);
/// <summary>Устанавливает целое число в ячейку с указанным числовым форматом.</summary>
ISheet Cell(uint row, uint col, int value, NumberFormatPattern? format = null);
/// <summary>Устанавливает длинное целое число в ячейку с указанным числовым форматом.</summary>
ISheet Cell(uint row, uint col, long value, NumberFormatPattern? format = null);
/// <summary>Возвращает ячейку по строке и букве столбца (например, 1, "A").</summary>
ICell Cell(uint row, string col);
/// <summary>Редактирует ячейку по строке и букве столбца.</summary>
ISheet Cell(uint row, string col, Action<ICell> edit);
/// <summary>Устанавливает строковое значение в ячейку по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, string value);
/// <summary>Устанавливает логическое значение в ячейку по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, bool value);
/// <summary>Устанавливает формулу в ячейку с числовым форматом по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает дату в ячейку по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, DateTime value, NumberFormatPattern? format = null);
/// <summary>Устанавливает десятичное число в ячейку по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, decimal value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число двойной точности в ячейку по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, double value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число с плавающей точкой в ячейку по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, float value, NumberFormatPattern? format = null);
/// <summary>Устанавливает целое число в ячейку по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, int value, NumberFormatPattern? format = null);
/// <summary>Устанавливает длинное целое число в ячейку по адресу (строка, буква столбца).</summary>
ISheet Cell(uint row, string col, long value, NumberFormatPattern? format = null);
/// <summary>Очищает всё содержимое листа (значения, формулы, но не форматирование).</summary>
void ClearContents();
/// <summary>Очищает всё форматирование на листе (стили, шрифты, границы, но оставляет значения).</summary>
void ClearFormats();
/// <summary>Очищает и содержимое, и форматирование листа.</summary>
void Clear();
/// <summary>Удаляет лист из книги</summary>
void Remove();
}
/// <summary>Представляет строку на листе.</summary>
public interface IRow
{
/// <summary>Индекс строки (начиная с 1).</summary>
uint Index { get; }
/// <summary>
/// Копирует текущую строку в указанную позицию (как "копировать-вставить" в Excel).
/// Исходная строка остаётся без изменений.
/// </summary>
/// <param name="index">Индекс строки (начиная с 1), в которую будет вставлена копия.
/// Существующие строки, начиная с этой позиции, сдвигаются вниз.</param>
/// <param name="copiedRow">Возвращает новую строку-копию, расположенную по указанному индексу.</param>
/// <returns>Текущий объект IRow для цепочки вызовов (fluent).</returns>
IRow CopyTo(uint index, out IRow copiedRow);
/// <summary>
/// Перемещает текущую строку в новую позицию (как "вырезать-вставить" в Excel).
/// Исходная строка удаляется, а перемещённая строка сохраняет все свои данные и форматирование.
/// </summary>
/// <param name="index">Новый индекс строки (начиная с 1).
/// Другие строки сдвигаются, освобождая место для перемещённой строки.</param>
/// <returns>Тот же объект IRow, но уже с новым индексом (fluent).</returns>
IRow MoveTo(uint index);
/// <summary>Высота строки</summary>
RowHeight Height { get; set; }
/// <summary>Устанавливает числовой формат для всех ячеек строки.</summary>
IRow SetNumberFormat(NumberFormatPattern format);
/// <summary>Возвращает ячейку в заданном столбце (индекс с 1).</summary>
ICell Cell(uint col);
/// <summary>Редактирует ячейку в заданном столбце.</summary>
IRow Cell(uint col, Action<ICell> edit);
/// <summary>Устанавливает строковое значение в ячейку столбца.</summary>
IRow Cell(uint col, string value);
/// <summary>Устанавливает логическое значение в ячейку столбца.</summary>
IRow Cell(uint col, bool value);
/// <summary>Устанавливает формулу в ячейку столбца с числовым форматом.</summary>
IRow Cell(uint col, string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает дату в ячейку столбца с числовым форматом.</summary>
IRow Cell(uint col, DateTime value, NumberFormatPattern? format = null);
/// <summary>Устанавливает десятичное число в ячейку столбца с числовым форматом.</summary>
IRow Cell(uint col, decimal value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число двойной точности в ячейку столбца с числовым форматом.</summary>
IRow Cell(uint col, double value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число с плавающей точкой в ячейку столбца с числовым форматом.</summary>
IRow Cell(uint col, float value, NumberFormatPattern? format = null);
/// <summary>Устанавливает целое число в ячейку столбца с числовым форматом.</summary>
IRow Cell(uint col, int value, NumberFormatPattern? format = null);
/// <summary>Устанавливает длинное целое число в ячейку столбца с числовым форматом.</summary>
IRow Cell(uint col, long value, NumberFormatPattern? format = null);
/// <summary>Возвращает ячейку по букве столбца.</summary>
ICell Cell(string col);
/// <summary>Редактирует ячейку по букве столбца.</summary>
IRow Cell(string col, Action<ICell> edit);
/// <summary>Устанавливает строковое значение в ячейку по букве столбца.</summary>
IRow Cell(string col, string value);
/// <summary>Устанавливает логическое значение в ячейку по букве столбца.</summary>
IRow Cell(string col, bool value);
/// <summary>Устанавливает формулу в ячейку по букве столбца с числовым форматом.</summary>
IRow Cell(string col, string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает дату в ячейку по букве столбца с числовым форматом.</summary>
IRow Cell(string col, DateTime value, NumberFormatPattern? format = null);
/// <summary>Устанавливает десятичное число в ячейку по букве столбца с числовым форматом.</summary>
IRow Cell(string col, decimal value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число двойной точности в ячейку по букве столбца с числовым форматом.</summary>
IRow Cell(string col, double value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число с плавающей точкой в ячейку по букве столбца с числовым форматом.</summary>
IRow Cell(string col, float value, NumberFormatPattern? format = null);
/// <summary>Устанавливает целое число в ячейку по букве столбца с числовым форматом.</summary>
IRow Cell(string col, int value, NumberFormatPattern? format = null);
/// <summary>Устанавливает длинное целое число в ячейку по букве столбца с числовым форматом.</summary>
IRow Cell(string col, long value, NumberFormatPattern? format = null);
/// <summary>Очищает содержимое всех ячеек строки (значения, формулы).</summary>
void ClearContents();
/// <summary>Очищает форматирование всех ячеек строки.</summary>
void ClearFormats();
/// <summary>Очищает содержимое и форматирование строки.</summary>
void Clear();
/// <summary>Удаляет строку с листа (сдвигая нижние строки вверх).</summary>
void Remove();
}
/// <summary>Представляет столбец на листе.</summary>
public interface IColumn
{
/// <summary>Индекс столбца (начиная с 1).</summary>
uint Index { get; }
/// <summary>Буквенное обозначение столбца (например, "A", "Z", "AA").</summary>
string IndexLetter { get; }
/// <summary>
/// Копирует текущий столбец в указанную позицию по числовому индексу (как "копировать-вставить").
/// Исходный столбец остаётся без изменений.
/// </summary>
/// <param name="index">Числовой индекс столбца (начиная с 1), куда будет вставлена копия.
/// Существующие столбцы, начиная с этой позиции, сдвигаются вправо.</param>
/// <param name="copiedColumn">Возвращает новый столбец-копию, расположенный по указанному индексу.</param>
/// <returns>Текущий объект IColumn для цепочки вызовов.</returns>
IColumn CopyTo(uint index, out IColumn copiedColumn);
/// <summary>
/// Копирует текущий столбец в указанную позицию по буквенному обозначению (как "копировать-вставить").
/// </summary>
/// <param name="index">Буквенное обозначение столбца (например, "D", "AA"), куда будет вставлена копия.</param>
/// <param name="copiedColumn">Возвращает новый столбец-копию.</param>
/// <returns>Текущий объект IColumn.</returns>
IColumn CopyTo(string index, out IColumn copiedColumn);
/// <summary>
/// Перемещает текущий столбец в новую позицию по числовому индексу (как "вырезать-вставить").
/// Исходный столбец удаляется, а перемещённый столбец сохраняет свои данные и форматирование.
/// </summary>
/// <param name="index">Новый числовой индекс столбца (начиная с 1).</param>
/// <returns>Тот же объект IColumn с новым индексом (fluent).</returns>
IColumn MoveTo(uint index);
/// <summary>
/// Перемещает текущий столбец в новую позицию по буквенному обозначению (как "вырезать-вставить").
/// </summary>
/// <param name="index">Новое буквенное обозначение столбца (например, "E").</param>
/// <returns>Тот же объект IColumn с новым индексом.</returns>
IColumn MoveTo(string index);
/// <summary>Ширина столбца (чтение и запись).</summary>
ColumnWidth Width { get; set; }
/// <summary>Устанавливает числовой формат для всех ячеек столбца.</summary>
IColumn SetNumberFormat(NumberFormatPattern format);
/// <summary>Возвращает ячейку в заданной строке (индекс с 1).</summary>
ICell Cell(uint row);
/// <summary>Редактирует ячейку в заданной строке.</summary>
IColumn Cell(uint row, Action<ICell> edit);
/// <summary>Устанавливает строковое значение в ячейку строки.</summary>
IColumn Cell(uint row, string value);
/// <summary>Устанавливает логическое значение в ячейку строки.</summary>
IColumn Cell(uint row, bool value);
/// <summary>Устанавливает формулу в ячейку строки с числовым форматом.</summary>
IColumn Cell(uint row, string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает дату в ячейку строки с числовым форматом.</summary>
IColumn Cell(uint row, DateTime value, NumberFormatPattern? format = null);
/// <summary>Устанавливает десятичное число в ячейку строки с числовым форматом.</summary>
IColumn Cell(uint row, decimal value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число двойной точности в ячейку строки с числовым форматом.</summary>
IColumn Cell(uint row, double value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число с плавающей точкой в ячейку строки с числовым форматом.</summary>
IColumn Cell(uint row, float value, NumberFormatPattern? format = null);
/// <summary>Устанавливает целое число в ячейку строки с числовым форматом.</summary>
IColumn Cell(uint row, int value, NumberFormatPattern? format = null);
/// <summary>Устанавливает длинное целое число в ячейку строки с числовым форматом.</summary>
IColumn Cell(uint row, long value, NumberFormatPattern? format = null);
/// <summary>Очищает содержимое всех ячеек столбца.</summary>
void ClearContents();
/// <summary>Очищает форматирование всех ячеек столбца.</summary>
void ClearFormats();
/// <summary>Очищает содержимое и форматирование столбца.</summary>
void Clear();
/// <summary>Удаляет столбец с листа (сдвигая правые столбцы влево).</summary>
void Remove();
}
/// <summary>Представляет прямоугольный диапазон ячеек на листе.</summary>
public interface IRange
{
/// <summary>Объединяет ячейки диапазона в одну (содержимое левой верхней ячейки сохраняется).</summary>
/// <returns>true, если объединение выполнено (диапазон не был объединён ранее).</returns>
bool TryMerge();
/// <summary>Разъединяет ранее объединённый диапазон (восстанавливает исходные ячейки, содержимое остаётся в левой верхней).</summary>
void Unmerge();
/// <summary>Проверяет, объединён ли диапазон (как единое целое).</summary>
bool IsMerged { get; }
/// <summary>Возвращает диапазон, представляющий объединённую область, если текущий диапазон является частью объединения.</summary>
IRange? GetMergedRange();
/// <summary>Пытается получить объединённый диапазон, содержащий текущий.</summary>
bool TryGetMergedRange(IRange range);
/// <summary>Номер начальной строки диапазона (1-based).</summary>
uint RowStart { get; }
/// <summary>Номер конечной строки.</summary>
uint RowEnd { get; }
/// <summary>Номер начального столбца.</summary>
uint ColStart { get; }
/// <summary>Номер конечного столбца.</summary>
uint ColEnd { get; }
/// <summary>Буквенное обозначение начального столбца.</summary>
string ColStartLetter { get; }
/// <summary>Буквенное обозначение конечного столбца.</summary>
string ColEndLetter { get; }
/// <summary>Количество строк в диапазоне.</summary>
uint Rows { get; }
/// <summary>Количество столбцов в диапазоне.</summary>
uint Cols { get; }
/// <summary>
/// Копирует текущий диапазон в новую позицию (верхний левый угол) как "копировать-вставить".
/// Исходный диапазон остаётся неизменным.
/// </summary>
/// <param name="rowIndex">Номер строки (начиная с 1) для верхнего левого угла вставляемого диапазона.</param>
/// <param name="colIndex">Номер столбца (начиная с 1) для верхнего левого угла.</param>
/// <param name="copiedRange">Возвращает новый диапазон-копию, расположенный по указанным координатам.</param>
/// <returns>Текущий объект IRange (fluent).</returns>
IRange CopyTo(uint rowIndex, uint colIndex, out IRange copiedRange);
/// <summary>
/// Копирует текущий диапазон в новую позицию (верхний левый угол) с указанием столбца буквой как "копировать-вставить".
/// Исходный диапазон остаётся неизменным.
/// </summary>
/// <param name="rowIndex">Номер строки (начиная с 1).</param>
/// <param name="colIndex">Буквенное обозначение столбца (например, "C").</param>
/// <param name="copiedRange">Возвращает новый диапазон-копию.</param>
/// <returns>Текущий объект IRange.</returns>
IRange CopyTo(uint rowIndex, string colIndex, out IRange copiedRange);
/// <summary>
/// Перемещает текущий диапазон в новую позицию (верхний левый угол) как "вырезать-вставить".
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
/// </summary>
/// <param name="rowIndex">Номер строки для нового положения верхнего левого угла.</param>
/// <param name="colIndex">Номер столбца для нового положения.</param>
/// <returns>Тот же объект IRange, но уже с новыми координатами (fluent).</returns>
IRange MoveTo(uint rowIndex, uint colIndex);
/// <summary>
/// Перемещает текущий диапазон в новую позицию (верхний левый угол) с указанием столбца буквой как "вырезать-вставить".
/// Исходный диапазон очищается, а текущий объект IRange перемещается на новое место.
/// </summary>
/// <param name="rowIndex">Номер строки.</param>
/// <param name="colIndex">Буквенное обозначение столбца.</param>
/// <returns>Тот же объект IRange с новыми координатами.</returns>
IRange MoveTo(uint rowIndex, string colIndex);
/// <summary>Устанавливает числовой формат для всех ячеек диапазона.</summary>
IRange SetNumberFormat(NumberFormatPattern format);
/// <summary>Устанавливает выравнивание для всех ячеек диапазона.</summary>
IRange SetCellAlign(CellAlign format);
/// <summary>Устанавливает границы для всех ячеек диапазона.</summary>
IRange SetCellBorder(CellBorder format);
/// <summary>Устанавливает заливку для всех ячеек диапазона.</summary>
IRange SetCellFill(CellFill format);
/// <summary>Устанавливает шрифт для всех ячеек диапазона.</summary>
IRange SetCellFont(CellFont format);
/// <summary>Перечисляет все ячейки диапазона (по строкам).</summary>
IEnumerable<ICell> Cells { get; }
/// <summary>Получает ячейку внутри диапазона по относительным координатам (начиная с 1).</summary>
bool GetSubCell(uint row, uint col, out ICell cell);
/// <summary>Редактирует ячейку внутри диапазона по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, Action<ICell> edit);
/// <summary>Устанавливает строковое значение в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, string value);
/// <summary>Устанавливает логическое значение в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, bool value);
/// <summary>Устанавливает формулу в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает дату в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, DateTime value, NumberFormatPattern? format = null);
/// <summary>Устанавливает десятичное число в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, decimal value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число двойной точности в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, double value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число с плавающей точкой в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, float value, NumberFormatPattern? format = null);
/// <summary>Устанавливает целое число в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, int value, NumberFormatPattern? format = null);
/// <summary>Устанавливает длинное целое число в ячейку по относительным координатам.</summary>
bool TryEditSubCell(uint row, uint col, long value, NumberFormatPattern? format = null);
/// <summary>Получает ячейку по относительной строке и букве столбца.</summary>
bool GetSubCell(uint row, string col, out ICell cell);
/// <summary>Редактирует ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, Action<ICell> edit);
/// <summary>Устанавливает строковое значение в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, string value);
/// <summary>Устанавливает логическое значение в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, bool value);
/// <summary>Устанавливает формулу в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает дату в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, DateTime value, NumberFormatPattern? format = null);
/// <summary>Устанавливает десятичное число в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, decimal value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число двойной точности в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, double value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число с плавающей точкой в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, float value, NumberFormatPattern? format = null);
/// <summary>Устанавливает целое число в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, int value, NumberFormatPattern? format = null);
/// <summary>Устанавливает длинное целое число в ячейку по относительной строке и букве столбца.</summary>
bool TryEditSubCell(uint row, string col, long value, NumberFormatPattern? format = null);
/// <summary>Очищает содержимое всех ячеек диапазона.</summary>
void ClearContents();
/// <summary>Очищает форматирование всех ячеек диапазона.</summary>
void ClearFormats();
/// <summary>Очищает и содержимое, и форматирование.</summary>
void Clear();
}
/// <summary>Представляет одну ячейку на листе.</summary>
public interface ICell
{
/// <summary>Проверяет, входит ли ячейка в объединённый диапазон.</summary>
bool IsMerged { get; }
/// <summary>Возвращает объединённый диапазон, если ячейка объединена, иначе null.</summary>
IRange? GetMergedRange();
/// <summary>Пытается получить объединённый диапазон, содержащий эту ячейку.</summary>
bool TryGetMergedRange(IRange range);
/// <summary>Номер строки (1-based).</summary>
uint Row { get; }
/// <summary>Номер столбца (1-based).</summary>
uint Col { get; }
/// <summary>Буква столбца.</summary>
string ColLetter { get; }
/// <summary>
/// Копирует текущую ячейку в указанную позицию (как "копировать-вставить").
/// Исходная ячейка остаётся без изменений.
/// </summary>
/// <param name="rowIndex">Номер строки (начиная с 1), куда будет вставлена копия.</param>
/// <param name="colIndex">Номер столбца (начиная с 1).</param>
/// <param name="copiedCell">Возвращает новую ячейку-копию.</param>
/// <returns>Текущий объект ICell для цепочки вызовов.</returns>
ICell CopyTo(uint rowIndex, uint colIndex, out ICell copiedCell);
/// <summary>
/// Копирует текущую ячейку в указанную позицию с буквенным обозначением столбца (как "копировать-вставить").
/// Исходная ячейка остаётся без изменений.
/// </summary>
/// <param name="rowIndex">Номер строки.</param>
/// <param name="colIndex">Буквенное обозначение столбца (например, "B").</param>
/// <param name="copiedCell">Возвращает новую ячейку-копию.</param>
/// <returns>Текущий объект ICell.</returns>
ICell CopyTo(uint rowIndex, string colIndex, out ICell copiedCell);
/// <summary>
/// Перемещает текущую ячейку в новую позицию (как "вырезать-вставить").
/// Исходная ячейка очищается, а текущий объект ICell перемещается в новое место.
/// </summary>
/// <param name="rowIndex">Новый номер строки.</param>
/// <param name="colIndex">Новый номер столбца.</param>
/// <returns>Тот же объект ICell с новыми координатами (fluent).</returns>
ICell MoveTo(uint rowIndex, uint colIndex);
/// <summary>
/// Перемещает текущую ячейку в новую позицию с буквенным обозначением столбца (как "вырезать-вставить").
/// Исходная ячейка очищается, а текущий объект ICell перемещается в новое место.
/// </summary>
/// <param name="rowIndex">Новый номер строки.</param>
/// <param name="colIndex">Буквенное обозначение столбца.</param>
/// <returns>Тот же объект ICell с новыми координатами.</returns>
ICell MoveTo(uint rowIndex, string colIndex);
/// <summary>Высота строки, содержащей ячейку (чтение и запись).</summary>
RowHeight Height { get; set; }
/// <summary>Ширина столбца, содержащего ячейку.</summary>
ColumnWidth Width { get; set; }
/// <summary>Проверяет, содержит ли ячейка числовое значение (целое или с плавающей точкой).</summary>
bool IsNumber { get; }
/// <summary>Проверяет, является ли содержимое логическим (TRUE/FALSE).</summary>
bool IsBoolean { get; }
/// <summary>Проверяет, содержит ли ячейка код ошибки (например, #DIV/0!).</summary>
bool IsError { get; }
/// <summary>Проверяет, интерпретируется ли значение как дата (по числовому формату).</summary>
bool IsDate { get; }
/// <summary>Проверяет, заблокирована ли ячейка (атрибут защиты).</summary>
bool IsLocked { get; }
/// <summary>Проверяет, содержит ли ячейка формулу.</summary>
bool HasFormula { get; }
/// <summary>Возвращает текстовое представление содержимого ячейки (как оно отображается).</summary>
string GetString();
/// <summary>Возвращает числовой формат ячейки.</summary>
NumberFormatPattern? GetNumberFormat();
/// <summary>Возвращает выравнивание текста ячейки.</summary>
CellAlign GetCellAlign();
/// <summary>Возвращает границы ячейки.</summary>
CellBorder GetCellBorder();
/// <summary>Возвращает заливку ячейки.</summary>
CellFill GetCellFill();
/// <summary>Возвращает шрифт ячейки.</summary>
CellFont GetCellFont();
/// <summary>Пытается извлечь логическое значение.</summary>
bool TryGetBoolean(out bool value);
/// <summary>Возвращает логическое значение или null, если тип не соответствует.</summary>
bool? GetBoolean();
/// <summary>Пытается извлечь дату и время.</summary>
bool TryGetDate(out DateTime value);
/// <summary>Возвращает дату или null.</summary>
DateTime? TryGetDate();
/// <summary>Пытается извлечь число (double).</summary>
bool TryGetNumber(out double value);
/// <summary>Возвращает число или null.</summary>
double? TryGetNumber();
/// <summary>Пытается установить формулу (без вычисленного значения).</summary>
/// <param name="formula">Текст формулы (например, "SUM(A1:A5)").</param>
/// <param name="format">Необязательный числовой формат для результата.</param>
bool TrySet(string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает формулу (выбрасывает исключение при ошибке).</summary>
ICell Set(string formula, NumberFormatPattern? format = null);
/// <summary>Устанавливает числовой формат ячейки (не меняя значение).</summary>
ICell Set(NumberFormatPattern format);
/// <summary>Устанавливает выравнивание текста ячейки.</summary>
ICell Set(CellAlign format);
/// <summary>Устанавливает границы ячейки.</summary>
ICell Set(CellBorder format);
/// <summary>Устанавливает заливку ячейки.</summary>
ICell Set(CellFill format);
/// <summary>Устанавливает шрифт ячейки.</summary>
ICell Set(CellFont format);
/// <summary>Устанавливает богатый текст (форматированный) с помощью делегата.</summary>
ICell Set(Action<ICellText> value);
/// <summary>Устанавливает простое текстовое значение (без форматирования).</summary>
ICell Set(string value);
/// <summary>Устанавливает логическое значение.</summary>
ICell Set(bool value);
/// <summary>Устанавливает дату, опционально с числовым форматом.</summary>
ICell Set(DateTime value, NumberFormatPattern? format = null);
/// <summary>Устанавливает десятичное число (сохраняется как double).</summary>
ICell Set(decimal value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число двойной точности.</summary>
ICell Set(double value, NumberFormatPattern? format = null);
/// <summary>Устанавливает число с плавающей точкой.</summary>
ICell Set(float value, NumberFormatPattern? format = null);
/// <summary>Устанавливает целое число.</summary>
ICell Set(int value, NumberFormatPattern? format = null);
/// <summary>Устанавливает длинное целое число.</summary>
ICell Set(long value, NumberFormatPattern? format = null);
/// <summary>Очищает содержимое ячейки (значение/формулу, но не форматирование).</summary>
void ClearContent();
/// <summary>Очищает форматирование ячейки (сбрасывает стиль).</summary>
void ClearFormat();
/// <summary>Очищает и содержимое, и форматирование.</summary>
void Clear();
}
/// <summary>Представляет богатый текст внутри ячейки (несколько форматированных фрагментов).</summary>
public interface ICellText
{
/// <summary>Количество фрагментов (Run) в тексте.</summary>
int Count { get; }
/// <summary>Возвращает все фрагменты.</summary>
IEnumerable<IRun> GetRuns();
/// <summary>Возвращает фрагмент по индексу или null.</summary>
IRun? GetRunAt(int index);
/// <summary>Пытается получить фрагмент по индексу.</summary>
bool TryGetRunAt(int index, out IRun run);
/// <summary>Первый фрагмент или null.</summary>
IRun? First();
/// <summary>Пытается получить первый фрагмент.</summary>
bool TryGetFirst(out IRun run);
/// <summary>Последний фрагмент или null.</summary>
IRun? Last();
/// <summary>Пытается получить последний фрагмент.</summary>
bool TryGetLast(out IRun run);
/// <summary>Пытается удалить фрагмент.</summary>
bool TryRemoveRun(IRun run);
/// <summary>Удаляет фрагмент по индексу.</summary>
bool TryRemoveRun(int index);
/// <summary>Удаляет фрагмент по индексу и возвращает удалённый.</summary>
bool TryRemoveRun(int index, out IRun? removed);
/// <summary>Добавляет разрыв строки (перенос внутри ячейки).</summary>
ICellText AddBreak();
/// <summary>Добавляет обычный текстовый фрагмент.</summary>
ICellText AddRun(string text, RunFormat? format = null);
/// <summary>Добавляет фрагмент с последующим разрывом строки.</summary>
ICellText AddRunBreak(string text, RunFormat? format = null);
/// <summary>Добавляет подстрочный фрагмент (эквивалентно AddRun с Vertical = Subscript).</summary>
ICellText AddSubRun(string text, RunFormat? format = null);
/// <summary>Добавляет надстрочный фрагмент.</summary>
ICellText AddSupRun(string text, RunFormat? format = null);
/// <summary>Вставляет фрагмент по индексу.</summary>
bool TryInsertRun(int index, string text, RunFormat? format = null);
/// <summary>Вставляет фрагмент с последующим разрывом строки по индексу.</summary>
bool TryInsertRunBreak(int index, string text, RunFormat? format = null);
/// <summary>Вставляет подстрочный фрагмент по индексу.</summary>
bool TryInsertSubRun(int index, string text, RunFormat? format = null);
/// <summary>Вставляет надстрочный фрагмент по индексу.</summary>
bool TryInsertSupRun(int index, string text, RunFormat? format = null);
/// <summary>Применяет заданный формат ко всем существующим фрагментам (поверх их текущего форматирования, заменяя неуказанные свойства).</summary>
void ApplyFormatToAllRuns(RunFormat format);
/// <summary>Удаляет все фрагменты, очищая текст ячейки.</summary>
void Clear();
}
/// <summary>Представляет один форматированный фрагмент текста внутри ячейки.</summary>
public interface IRun
{
/// <summary>Текст фрагмента.</summary>
string Text { get; set; }
/// <summary>Форматирование фрагмента (может быть null, что означает отсутствие явного форматирования).</summary>
RunFormat? Format { get; set; }
}

View File

@@ -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;
}

View File

@@ -0,0 +1,29 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Предоставляет типобезопасное представление высоты строки в Excel с поддержкой различных единиц измерения.
/// </summary>
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;
/// <summary>Высота в пунктах (points).</summary>
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;
}

View File

@@ -0,0 +1,70 @@
namespace QWERTYkez.ExcelProcessor.Editors;
/// <summary>
/// Определяет форматирование отдельного фрагмента (Run) внутри ячейки с богатым текстом.
/// Применяется только к тексту внутри <see cref="ICellText"/>.
/// </summary>
public readonly struct RunFormat
{
/// <summary>Жирное начертание фрагмента.</summary>
public bool? IsBold { get; init; }
/// <summary>Курсив фрагмента.</summary>
public bool? IsItalic { get; init; }
/// <summary>Стиль подчёркивания (одинарное, двойное, волнистое и т.д.).</summary>
public UnderlineStyle? Underline { get; init; }
/// <summary>Одинарное зачёркивание.</summary>
public bool? IsStrike { get; init; }
/// <summary>Цвет текста фрагмента.</summary>
public ExColor? Color { get; init; }
/// <summary>Размер шрифта фрагмента в пунктах.</summary>
public double? FontSize { get; init; }
/// <summary>Имя шрифта фрагмента (например, "Calibri").</summary>
public string? FontFamily { get; init; }
/// <summary>Вертикальное смещение (надстрочный или подстрочный).</summary>
public VerticalTextRunAlignment? Vertical { get; init; }
/*
методы для извлечения OpenXmlElement или других более удобных типов
public bool TryExtract(out List<...> elements);
или
public bool TrySetFor(InlineString str)
или
public bool TrySetFor(ExcelRun str)
*/
}
/// <summary>Определяет стиль подчёркивания текста в ячейке или в части текста (Run).</summary>
public enum UnderlineStyle
{
/// <summary>Одинарное сплошное подчёркивание.</summary>
Single,
/// <summary>Двойное сплошное подчёркивание.</summary>
Double,
/// <summary>Одинарное подчёркивание, используемое для бухгалтерских форматов (нижняя граница ячейки).</summary>
SingleAccounting,
/// <summary>Двойное подчёркивание для бухгалтерских форматов.</summary>
DoubleAccounting
}
/// <summary>Вертикальное смещение текста внутри прогона (Run) надстрочный или подстрочный.</summary>
public enum VerticalTextRunAlignment
{
/// <summary>Надстрочный текст (верхний индекс).</summary>
Superscript,
/// <summary>Подстрочный текст (нижний индекс).</summary>
Subscript
}

View File

@@ -0,0 +1,320 @@
namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Статический класс для работы с документами Excel через процессоры чтения/записи.
/// </summary>
public static class ExcelProcessor
{
#region Read Operations
/// <summary>
/// Пытается открыть документ только для чтения и выполнить действия.
/// </summary>
/// <param name="sourcePath">Путь к исходному файлу .xlsx</param>
/// <param name="read">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryRead(string sourcePath, Action<IExcelReader> read)
{
return TryRead(new FileInfo(sourcePath), read);
}
/// <summary>
/// Пытается открыть документ только для чтения и выполнить действия.
/// </summary>
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .xlsx</param>
/// <param name="read">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryRead(FileInfo sourceFile, Action<IExcelReader> 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
/// <summary>
/// Пытается открыть документ из массива байт только для чтения и выполнить действия.
/// </summary>
/// <param name="data">Массив байт, содержащий документ .xlsx</param>
/// <param name="read">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryRead(byte[] data, Action<IExcelReader> read)
=> TryRead(new ReadOnlyMemory<byte>(data), read);
/// <summary>
/// Пытается открыть документ из <see cref="ReadOnlyMemory{byte}"/> только для чтения и выполнить действия.
/// </summary>
/// <param name="data">Буфер с документом .xlsx</param>
/// <param name="read">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryRead(ReadOnlyMemory<byte> data, Action<IExcelReader> 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
/// <summary>
/// Пытается открыть документ для записи и выполнить действия с последующей перезаписью исходного файла.
/// </summary>
/// <param name="sourcePath">Путь к исходному файлу .xlsx</param>
/// <param name="write">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryWrite(string sourcePath, Action<IExcelWriter> write)
{
return TryWrite(sourcePath, sourcePath, write);
}
/// <summary>
/// Пытается открыть документ для записи и выполнить действия с последующей перезаписью исходного файла.
/// </summary>
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .xlsx</param>
/// <param name="write">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryWrite(FileInfo sourceFile, Action<IExcelWriter> write)
{
return TryWrite(sourceFile, sourceFile.FullName, write);
}
/// <summary>
/// Пытается открыть документ для записи, выполнить действия и сохранить результат по новому пути.
/// </summary>
/// <param name="sourcePath">Путь к исходному файлу .xlsx</param>
/// <param name="destinationPath">Путь для сохранения результата (null - перезапись исходного файла)</param>
/// <param name="write">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryWrite(string sourcePath, string? destinationPath, Action<IExcelWriter> write)
{
return TryWrite(new FileInfo(sourcePath), destinationPath, write);
}
/// <summary>
/// Пытается открыть документ для записи, выполнить действия и сохранить результат по новому пути.
/// </summary>
/// <param name="sourceFile">Объект <see cref="FileInfo"/> исходного файла .xlsx</param>
/// <param name="destinationPath">Путь для сохранения результата (null - перезапись исходного файла)</param>
/// <param name="write">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryWrite(FileInfo sourceFile, string? destinationPath, Action<IExcelWriter> 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
/// <summary>
/// Пытается открыть документ из массива байт для записи, выполнить действия и сохранить результат по указанному пути.
/// </summary>
/// <param name="data">Массив байт, содержащий документ .xlsx</param>
/// <param name="destinationPath">Путь для сохранения результата</param>
/// <param name="write">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryWrite(byte[] data, string destinationPath, Action<IExcelWriter> write)
=> TryWrite(new ReadOnlyMemory<byte>(data), destinationPath, write);
/// <summary>
/// Пытается открыть документ из <see cref="ReadOnlyMemory{byte}"/> для записи, выполнить действия и сохранить результат по указанному пути.
/// </summary>
/// <param name="data">Буфер с документом .xlsx</param>
/// <param name="destinationPath">Путь для сохранения результата</param>
/// <param name="write">Действия для выполнения над документом</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/></returns>
public static bool TryWrite(ReadOnlyMemory<byte> data, string destinationPath, Action<IExcelWriter> 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
/// <summary>
/// Пытается создать новый документ Excel, отредактировать его и вернуть результат в виде массива байт.
/// </summary>
/// <param name="result">Результирующий массив байт (если операция успешна).</param>
/// <param name="write">Действия для заполнения документа.</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
public static bool TryCreate(out byte[] result, Action<IExcelWriter> 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;
}
}
/// <summary>
/// Пытается создать новый документ Excel, отредактировать его и записать в указанный поток.
/// </summary>
/// <param name="outputStream">Поток, в который будет записан документ.</param>
/// <param name="write">Действия для заполнения документа.</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
public static bool TryCreate(Stream outputStream, Action<IExcelWriter> 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;
}
}
/// <summary>
/// Пытается создать новый документ Excel, отредактировать его и сохранить на диск.
/// </summary>
/// <param name="destinationPath">Путь для сохранения результата.</param>
/// <param name="write">Действия для заполнения документа.</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
public static bool TryCreate(string destinationPath, Action<IExcelWriter> 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;
}
}
/// <summary>
/// Пытается создать новый документ Excel, отредактировать его и прочитать результат в памяти.
/// </summary>
/// <param name="write">Действия для заполнения документа.</param>
/// <param name="read">Действия для чтения полученного документа.</param>
/// <returns><see langword="true"/> если операция выполнена успешно, иначе <see langword="false"/>.</returns>
public static bool TryRead(Action<IExcelWriter> write, Action<IExcelReader> 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;
}
}
}

View File

@@ -0,0 +1,276 @@
namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Предоставляет потокобезопасный процессор только для чтения документов Excel (xlsx / xlsm) формата.
/// <para>Не поддерживает операции изменения документа.</para>
/// </summary>
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<byte> 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<IExcelWriter> 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<byte>(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<IExcelWriter> write, Action<IExcelReader> 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
/// <summary>
/// Получает путь к исходному файлу.
/// </summary>
public string? FilePath { get; protected set; }
/// <summary>
/// Получает значение, указывающее, находится ли процессор в допустимом состоянии для операций.
/// </summary>
public bool IsValid => !_disposed && _doc is not null && _ms is not null;
/// <summary>
/// Получает текущий размер документа в байтах.
/// </summary>
public long DocumentSize => _ms.Length;
#endregion
#region Read Operations
/// <summary>
/// Находит все уникальные плейсхолдеры в формате $...$ в документе.
/// <para>Ищет только внутри параграфов. Игнорирует вхождения, которые пересекают границы параграфов.</para>
/// </summary>
/// <param name="comparisonType">Способ сравнения строк при поиске (по умолчанию: без учета регистра)</param>
/// <returns>Коллекция уникальных найденных плейсхолдеров</returns>
public ISet<string> 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
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
namespace QWERTYkez.ExcelProcessor;
public interface IExcelReader
{
long DocumentSize { get; }
string? FilePath { get; }
bool IsValid { get; }
ISet<string> FindPlaceholders();
bool TryWrite(string destinationPath, Action<IExcelWriter> action);
bool TryWrite(Action<IExcelWriter> write, Action<IExcelReader> read);
}

View File

@@ -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<string, string> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IDictionary<string, double> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IDictionary<string, float> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IDictionary<string, int> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IDictionary<string, long> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IEnumerable<KeyValuePair<string, double>> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IEnumerable<KeyValuePair<string, float>> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IEnumerable<KeyValuePair<string, int>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Replace(IEnumerable<KeyValuePair<string, long>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase);
void Edit(Action<IBook> edit);
}

View File

@@ -0,0 +1,135 @@
using System.Collections;
using System.Globalization;
namespace QWERTYkez.ExcelProcessor;
/// <summary>
/// Множество строк, которое автоматически приводит все добавляемые элементы
/// к верхнему регистру и удаляет диакритические знаки (например, 'ё' -> 'Е').
/// Реализует ISet&lt;string&gt;, поэтому может использоваться там, где ожидается этот интерфейс.
/// </summary>
public class NormalizedSet : ISet<string>
{
private readonly HashSet<string> _inner;
/// <summary>
/// Создаёт пустое нормализованное множество.
/// </summary>
public NormalizedSet()
{
_inner = [];
}
/// <summary>
/// Создаёт нормализованное множество, заполненное элементами из указанной коллекции.
/// </summary>
/// <param name="collection">Коллекция, элементы которой будут нормализованы и добавлены.</param>
public NormalizedSet(IEnumerable<string> collection)
{
_inner = [.. collection.Select(Normalize)];
}
/// <summary>
/// Нормализует строку: верхний регистр и удаление диакритики.
/// </summary>
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<string> ----------
public bool Add(string item) => _inner.Add(Normalize(item));
void ICollection<string>.Add(string item) => Add(item);
public void UnionWith(IEnumerable<string> other)
{
foreach (var item in other)
Add(item);
}
public void IntersectWith(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
_inner.IntersectWith(normalizedOther);
}
public void ExceptWith(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
_inner.ExceptWith(normalizedOther);
}
public void SymmetricExceptWith(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
_inner.SymmetricExceptWith(normalizedOther);
}
public bool IsSubsetOf(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
return _inner.IsSubsetOf(normalizedOther);
}
public bool IsSupersetOf(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
return _inner.IsSupersetOf(normalizedOther);
}
public bool IsProperSupersetOf(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
return _inner.IsProperSupersetOf(normalizedOther);
}
public bool IsProperSubsetOf(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
return _inner.IsProperSubsetOf(normalizedOther);
}
public bool Overlaps(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
return _inner.Overlaps(normalizedOther);
}
public bool SetEquals(IEnumerable<string> other)
{
var normalizedOther = new HashSet<string>(other.Select(Normalize));
return _inner.SetEquals(normalizedOther);
}
// ---------- Реализация ICollection<string> ----------
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<string> и IEnumerable ----------
public IEnumerator<string> GetEnumerator() => _inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

View File

@@ -0,0 +1,143 @@
namespace QWERTYkez.ExcelProcessor;
internal static class PlaceholderFinder
{
/// <summary>Находит плейсхолдеры $...$ в указанных листах.</summary>
public static ISet<string> 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;
}
/// <summary>Находит плейсхолдеры во всех листах документа.</summary>
public static ISet<string> 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<string> 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<string> result)
{
var sheetData = worksheet.GetFirstChild<SheetData>();
if (sheetData is null) return;
List<string>? tempList = null; // буфер для плейсхолдеров из одного текста
foreach (var row in sheetData.Elements<Row>())
{
foreach (var cell in row.Elements<Cell>())
{
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<string> result)
{
var hf = worksheet.Descendants<HeaderFooter>().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<string>? 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<Text>().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<Text>().Select(t => t.Text));
}
return string.Empty;
}
return raw;
}
private static unsafe bool FindPlaceholdersInText(string text, ref List<string>? 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;
}
}
}

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.5.1" />
<PackageReference Include="IsExternalInit" Version="1.0.3" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="System.Drawing.Common" Version="10.0.8" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,524 @@
using System.Globalization;
namespace QWERTYkez.ExcelProcessor;
internal static class ReplaceNumericExtensions
{
// =========================== МНОЖЕСТВЕННЫЕ ЗАМЕНЫ ===========================
// --- Double ---
internal static void Replace(this SpreadsheetDocument doc,
IEnumerable<KeyValuePair<string, double>> 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<KeyValuePair<string, double>>;
int count = coll?.Count ?? replacements.Count();
if (count == 0) return;
var workbookPart = doc.WorkbookPart!;
var stringReplacements = new Dictionary<string, string>(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<KeyValuePair<string, float>> 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<KeyValuePair<string, float>>;
int count = coll?.Count ?? replacements.Count();
if (count == 0) return;
var workbookPart = doc.WorkbookPart!;
var stringReplacements = new Dictionary<string, string>(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<KeyValuePair<string, int>> 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<KeyValuePair<string, int>>;
int count = coll?.Count ?? replacements.Count();
if (count == 0) return;
var workbookPart = doc.WorkbookPart!;
var stringReplacements = new Dictionary<string, string>(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<KeyValuePair<string, long>> 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<KeyValuePair<string, long>>;
int count = coll?.Count ?? replacements.Count();
if (count == 0) return;
var workbookPart = doc.WorkbookPart!;
var stringReplacements = new Dictionary<string, string>(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));
}
// =========================== ОДИНОЧНЫЕ ЗАМЕНЫ ===========================
// --- Double ---
internal static void Replace(this SpreadsheetDocument doc,
string oldValue, double newValue, string? format = null,
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
{
if (string.IsNullOrEmpty(oldValue)) return;
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
if (worksheets.Length < 1) return;
var workbookPart = doc.WorkbookPart!;
ReplaceSingleCore(workbookPart, worksheets, oldValue, newValue, format, comparisonType,
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture),
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.CurrentCulture));
}
// --- Float ---
internal static void Replace(this SpreadsheetDocument doc,
string oldValue, float newValue, string? format = null,
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
{
if (string.IsNullOrEmpty(oldValue)) return;
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
if (worksheets.Length < 1) return;
var workbookPart = doc.WorkbookPart!;
ReplaceSingleCore(workbookPart, worksheets, oldValue, newValue, format, comparisonType,
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture),
(val, fmt) => val.ToString(fmt ?? "G", CultureInfo.CurrentCulture));
}
// --- Int ---
internal static void Replace(this SpreadsheetDocument doc,
string oldValue, int newValue,
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
{
if (string.IsNullOrEmpty(oldValue)) return;
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
if (worksheets.Length < 1) return;
var workbookPart = doc.WorkbookPart!;
ReplaceSingleCore(workbookPart, worksheets, oldValue, newValue, null, comparisonType,
(val, _) => val.ToString(CultureInfo.InvariantCulture),
(val, _) => val.ToString(CultureInfo.CurrentCulture));
}
// --- Long ---
internal static void Replace(this SpreadsheetDocument doc,
string oldValue, long newValue,
StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
{
if (string.IsNullOrEmpty(oldValue)) return;
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
if (worksheets.Length < 1) return;
var workbookPart = doc.WorkbookPart!;
ReplaceSingleCore(workbookPart, worksheets, oldValue, newValue, null, comparisonType,
(val, _) => val.ToString(CultureInfo.InvariantCulture),
(val, _) => val.ToString(CultureInfo.CurrentCulture));
}
// =========================== ОБЩАЯ ЛОГИКА ===========================
private static void ReplaceNumericCore<T>(
WorkbookPart workbookPart,
WorksheetPart[] worksheets,
IEnumerable<KeyValuePair<string, T>> numericReplacements,
Dictionary<string, string> stringReplacements,
string? format,
StringComparison comparisonType,
Func<T, string?, string> numberToStringForNumberCell)
{
var formatCache = new Dictionary<string, uint>();
uint? GetOrCreateStyleIndex(string fmt)
{
if (string.IsNullOrEmpty(fmt)) return null;
if (formatCache.TryGetValue(fmt, out var idx)) return idx;
var newIdx = CreateNumberFormat(workbookPart, fmt);
formatCache[fmt] = newIdx;
return newIdx;
}
// Инициализация SharedStringTable (один раз)
var allSharedStrings = new List<string>();
var sharedStringIndexMap = new Dictionary<string, int>();
var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable;
if (sharedStringTable != null)
{
foreach (var item in sharedStringTable.Elements<SharedStringItem>())
{
var text = ConcatTexts(item.Descendants<Text>());
sharedStringIndexMap[text] = allSharedStrings.Count;
allSharedStrings.Add(text);
}
}
foreach (var worksheetPart in worksheets)
{
var worksheet = worksheetPart.Worksheet;
if (worksheet == null) continue;
var sheetData = worksheet.GetFirstChild<SheetData>();
if (sheetData == null) continue;
// Обработка ячеек
foreach (var row in sheetData.Elements<Row>())
{
foreach (var cell in row.Elements<Cell>())
{
string originalText = GetCellTextForNumeric(cell, allSharedStrings);
if (string.IsNullOrEmpty(originalText)) continue;
string? matchedKey = null;
T matchedVal = default!;
int matchStart = -1, matchLength = 0;
foreach (var kvp in numericReplacements)
{
int idx = originalText.IndexOf(kvp.Key, comparisonType);
if (idx >= 0 && kvp.Key.Length > matchLength)
{
matchedKey = kvp.Key;
matchedVal = kvp.Value;
matchStart = idx;
matchLength = kvp.Key.Length;
}
}
if (matchedKey == null) continue;
bool isFullCell = (matchStart == 0 && matchLength == originalText.Length);
if (isFullCell)
{
cell.DataType = CellValues.Number;
string numStr = numberToStringForNumberCell(matchedVal, format);
cell.CellValue = new CellValue(numStr);
if (!string.IsNullOrEmpty(format))
{
var styleIdx = GetOrCreateStyleIndex(format!);
if (styleIdx.HasValue)
cell.StyleIndex = styleIdx.Value;
}
}
else
{
string replacementStr = stringReplacements[matchedKey];
string newText = ReplaceSubstring(originalText, matchStart, matchLength, replacementStr);
SetCellText(cell, newText, allSharedStrings, sharedStringIndexMap);
}
}
}
// Колонтитулы и комментарии
ReplaceInHeadersFooters(worksheetPart, stringReplacements, comparisonType);
ReplaceInComments(worksheetPart, stringReplacements, comparisonType);
}
// Сохраняем SharedStringTable
UpdateSharedStringTable(workbookPart, allSharedStrings);
}
private static void ReplaceSingleCore<T>(
WorkbookPart workbookPart,
WorksheetPart[] worksheets,
string oldValue,
T newValue,
string? format,
StringComparison comparisonType,
Func<T, string?, string> numberToStringForNumberCell,
Func<T, string?, string> numberToStringForTextCell)
{
var formatCache = new Dictionary<string, uint>();
uint? GetOrCreateStyleIndex(string fmt)
{
if (string.IsNullOrEmpty(fmt)) return null;
if (formatCache.TryGetValue(fmt, out var idx)) return idx;
var newIdx = CreateNumberFormat(workbookPart, fmt);
formatCache[fmt] = newIdx;
return newIdx;
}
var singleStringReplacement = new Dictionary<string, string>
{
{ oldValue, numberToStringForTextCell(newValue, format) }
};
// Инициализация SharedStringTable
var allSharedStrings = new List<string>();
var sharedStringIndexMap = new Dictionary<string, int>();
var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable;
if (sharedStringTable != null)
{
foreach (var item in sharedStringTable.Elements<SharedStringItem>())
{
var text = ConcatTexts(item.Descendants<Text>());
sharedStringIndexMap[text] = allSharedStrings.Count;
allSharedStrings.Add(text);
}
}
foreach (var worksheetPart in worksheets)
{
var worksheet = worksheetPart.Worksheet;
if (worksheet == null) continue;
var sheetData = worksheet.GetFirstChild<SheetData>();
if (sheetData == null) continue;
foreach (var row in sheetData.Elements<Row>())
{
foreach (var cell in row.Elements<Cell>())
{
string originalText = GetCellTextForNumeric(cell, allSharedStrings);
if (string.IsNullOrEmpty(originalText)) continue;
int idx = originalText.IndexOf(oldValue, comparisonType);
if (idx < 0) continue;
bool isFullCell = (idx == 0 && oldValue.Length == originalText.Length);
if (isFullCell)
{
cell.DataType = CellValues.Number;
string numStr = numberToStringForNumberCell(newValue, format);
cell.CellValue = new CellValue(numStr);
if (!string.IsNullOrEmpty(format))
{
var styleIdx = GetOrCreateStyleIndex(format!);
if (styleIdx.HasValue)
cell.StyleIndex = styleIdx.Value;
}
}
else
{
string replacementStr = numberToStringForTextCell(newValue, format);
string newText = ReplaceSubstring(originalText, idx, oldValue.Length, replacementStr);
SetCellText(cell, newText, allSharedStrings, sharedStringIndexMap);
}
}
}
ReplaceInHeadersFooters(worksheetPart, singleStringReplacement, comparisonType);
ReplaceInComments(worksheetPart, singleStringReplacement, comparisonType);
}
UpdateSharedStringTable(workbookPart, allSharedStrings);
}
// =========================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ===========================
private static string GetCellTextForNumeric(Cell cell, List<string> allSharedStrings)
{
if (cell?.CellValue == null) return string.Empty;
// InlineString без LINQ
if (cell.InlineString != null)
{
var sb = new StringBuilder();
foreach (var t in cell.InlineString.Descendants<Text>())
sb.Append(t.Text);
return sb.ToString();
}
string val = cell.CellValue.InnerText;
if (cell.DataType?.Value == CellValues.SharedString)
{
if (int.TryParse(val, out int idx) && idx >= 0 && idx < allSharedStrings.Count)
return allSharedStrings[idx];
return string.Empty;
}
return val;
}
private static void SetCellText(Cell cell, string newText,
List<string> allSharedStrings, Dictionary<string, int> sharedStringIndexMap)
{
if (cell.InlineString != null)
{
// Очищаем старые тексты
foreach (var t in cell.InlineString.Descendants<Text>().ToList())
t.Remove();
cell.InlineString.AppendChild(new Text(newText));
return;
}
if (!sharedStringIndexMap.TryGetValue(newText, out int index))
{
index = allSharedStrings.Count;
allSharedStrings.Add(newText);
sharedStringIndexMap[newText] = index;
}
cell.DataType = CellValues.SharedString;
cell.CellValue = new CellValue(index.ToString());
}
private static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings)
{
var ssPart = workbookPart.SharedStringTablePart;
ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>();
var sharedStringTable = ssPart.SharedStringTable ?? new SharedStringTable();
sharedStringTable.RemoveAllChildren<SharedStringItem>();
foreach (var str in allSharedStrings)
sharedStringTable.AppendChild(new SharedStringItem(new Text(str)));
sharedStringTable.Save();
}
private static string ConcatTexts(IEnumerable<Text> texts)
{
var sb = new StringBuilder();
foreach (var t in texts)
sb.Append(t.Text);
return sb.ToString();
}
// Оптимизированная замена подстроки через string.Create (без unsafe)
private static unsafe string ReplaceSubstring(string original, int start, int length, string replacement)
{
if (length == 0) return original;
int newLen = original.Length - length + replacement.Length;
if (newLen <= 0) return replacement;
fixed (char* pOrig = original, pRep = replacement)
{
char* result = stackalloc char[newLen];
int pos = 0;
for (int i = 0; i < start; i++)
result[pos++] = pOrig[i];
for (int i = 0; i < replacement.Length; i++)
result[pos++] = pRep[i];
for (int i = start + length; i < original.Length; i++)
result[pos++] = pOrig[i];
return new string(result, 0, newLen);
}
}
private static uint CreateNumberFormat(WorkbookPart workbookPart, string format)
{
var stylesPart = workbookPart.WorkbookStylesPart;
if (stylesPart == null)
{
stylesPart = workbookPart.AddNewPart<WorkbookStylesPart>();
stylesPart.Stylesheet = new Stylesheet();
}
var ss = stylesPart.Stylesheet!;
ss.NumberingFormats ??= new NumberingFormats();
uint nextId = 164;
if (ss.NumberingFormats.Elements<NumberingFormat>().Any())
nextId = ss.NumberingFormats.Elements<NumberingFormat>().Max(nf => nf.NumberFormatId!.Value) + 1;
var nf = new NumberingFormat { NumberFormatId = nextId, FormatCode = format };
ss.NumberingFormats.AppendChild(nf);
ss.CellFormats ??= new CellFormats();
var cf = new DocumentFormat.OpenXml.Spreadsheet.CellFormat
{
NumberFormatId = nextId,
FormatId = 0,
ApplyNumberFormat = true
};
ss.CellFormats.AppendChild(cf);
ss.Save();
return ss.CellFormats.Count!.Value - 1;
}
// =========================== КОЛОНТИТУЛЫ И КОММЕНТАРИИ ===========================
private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
var worksheet = worksheetPart.Worksheet;
if (worksheet is null) return;
var headerFooter = worksheet.Descendants<HeaderFooter>().FirstOrDefault();
if (headerFooter is null) return;
foreach (var elem in new OpenXmlLeafTextElement?[] { headerFooter.OddHeader, headerFooter.OddFooter, headerFooter.EvenHeader, headerFooter.EvenFooter, headerFooter.FirstHeader, headerFooter.FirstFooter })
ReplaceHeaderFooter(elem, replacementDict, comparisonType);
}
private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
if (element?.Text is null) return;
string original = element.Text;
string processed = ProcessReplacements(original, replacementDict, comparisonType);
if (processed != original)
element.Text = processed;
}
private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
var commentsPart = worksheetPart.WorksheetCommentsPart;
if (commentsPart?.Comments is null) return;
foreach (var comment in commentsPart.Comments.Elements<Comment>())
{
var textElement = comment.Descendants<CommentText>().FirstOrDefault();
if (textElement?.Text is null) continue;
string original = textElement.Text.Text;
if (string.IsNullOrEmpty(original)) continue;
string processed = ProcessReplacements(original, replacementDict, comparisonType);
if (processed != original)
textElement.Text.Text = processed;
}
}
private static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input;
string result = input;
foreach (string key in replacementDict.Keys.OrderByDescending(k => k.Length))
{
string value = replacementDict[key];
result = ReplaceInString(result, key, value, comparisonType);
}
return result;
}
private static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType)
{
int idx = original.IndexOf(oldValue, comparisonType);
if (idx < 0) return original;
var sb = new StringBuilder(original.Length + newValue.Length - oldValue.Length);
int last = 0;
while (idx >= 0)
{
sb.Append(original, last, idx - last);
sb.Append(newValue);
last = idx + oldValue.Length;
idx = original.IndexOf(oldValue, last, comparisonType);
}
sb.Append(original, last, original.Length - last);
return sb.ToString();
}
}

View File

@@ -0,0 +1,217 @@
namespace QWERTYkez.ExcelProcessor;
internal static class ReplaceStringExtensions
{
// --- Публичный метод для одиночной замены ---
internal static void Replace(this SpreadsheetDocument doc, string oldValue, string newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
{
if (string.IsNullOrEmpty(oldValue)) return;
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
if (worksheets.Length < 1) return;
var replacement = new Dictionary<string, string> { [oldValue] = newValue };
ReplaceCore(doc, worksheets, replacement, comparisonType);
}
// --- Публичный метод для множественной замены ---
internal static void Replace(this SpreadsheetDocument doc, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
{
if (replacements is null) return;
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
if (worksheets.Length < 1) return;
var comparer = GetComparerForStringComparison(comparisonType);
var replacementDict = new Dictionary<string, string>(comparer);
foreach (var kvp in replacements)
if (!string.IsNullOrEmpty(kvp.Key))
replacementDict[kvp.Key] = kvp.Value;
if (replacementDict.Count == 0) return;
ReplaceCore(doc, worksheets, replacementDict, comparisonType);
}
// --- Общий приватный метод, содержащий всю логику замены ---
private static void ReplaceCore(SpreadsheetDocument doc, WorksheetPart[] worksheets, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
var workbookPart = doc.WorkbookPart!;
// 1. Собрать все изменения для ячеек
var cellChanges = new Dictionary<Cell, string>();
var allSharedStrings = new List<string>();
var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable;
if (sharedStringTable is not null)
{
foreach (var item in sharedStringTable.Elements<SharedStringItem>())
allSharedStrings.Add(string.Concat(item.Descendants<Text>().Select(t => t.Text)));
}
foreach (var worksheetPart in worksheets)
CollectCellChanges(worksheetPart, replacementDict, comparisonType, cellChanges, allSharedStrings);
// 2. Применить изменения к ячейкам
ApplyCellChanges(cellChanges, allSharedStrings);
// 3. Обновить SharedStringTable
UpdateSharedStringTable(workbookPart, allSharedStrings);
// 4. Обработать колонтитулы
foreach (var worksheetPart in worksheets)
ReplaceInHeadersFooters(worksheetPart, replacementDict, comparisonType);
// 5. Обработать комментарии
foreach (var worksheetPart in worksheets)
ReplaceInComments(worksheetPart, replacementDict, comparisonType);
}
// --- Остальные вспомогательные методы (без изменений) ---
private static IEqualityComparer<string> GetComparerForStringComparison(StringComparison comparisonType) =>
comparisonType switch
{
StringComparison.Ordinal => StringComparer.Ordinal,
StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase,
StringComparison.InvariantCulture => StringComparer.InvariantCulture,
StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase,
StringComparison.CurrentCulture => StringComparer.CurrentCulture,
StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase,
_ => StringComparer.OrdinalIgnoreCase,
};
private static void CollectCellChanges(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType, Dictionary<Cell, string> cellChanges, List<string> allSharedStrings)
{
var worksheet = worksheetPart.Worksheet;
if (worksheet is null) return;
var sheetData = worksheet.GetFirstChild<SheetData>();
if (sheetData is null) return;
foreach (var row in sheetData.Elements<Row>())
foreach (var cell in row.Elements<Cell>())
{
string originalText = GetCellText(cell, allSharedStrings);
if (string.IsNullOrEmpty(originalText)) continue;
string newText = ProcessReplacements(originalText, replacementDict, comparisonType);
if (newText != originalText)
cellChanges[cell] = newText;
}
}
private static void ApplyCellChanges(Dictionary<Cell, string> cellChanges, List<string> allSharedStrings)
{
foreach (var kvp in cellChanges)
{
var cell = kvp.Key;
var newText = kvp.Value;
if (cell.InlineString is not null)
{
var texts = cell.InlineString.Descendants<Text>().ToList();
foreach (var t in texts) t.Remove();
cell.InlineString.AppendChild(new Text(newText));
}
else
{
int newIndex = AddOrFindStringIndex(allSharedStrings, newText);
cell.DataType = new EnumValue<CellValues>(CellValues.SharedString);
cell.CellValue = new CellValue(newIndex.ToString());
}
}
}
private static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings)
{
var ssPart = workbookPart.SharedStringTablePart;
ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>();
var sharedStringTable = ssPart.SharedStringTable ?? new SharedStringTable();
sharedStringTable.RemoveAllChildren<SharedStringItem>();
foreach (var str in allSharedStrings)
sharedStringTable.AppendChild(new SharedStringItem(new Text(str)));
sharedStringTable.Save();
}
private static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input;
string result = input;
foreach (string key in replacementDict.Keys.OrderByDescending(k => k.Length))
{
string value = replacementDict[key];
result = ReplaceInString(result, key, value, comparisonType);
}
return result;
}
private static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType)
{
int idx = original.IndexOf(oldValue, comparisonType);
if (idx < 0) return original;
var sb = new StringBuilder(original.Length + newValue.Length - oldValue.Length);
int last = 0;
while (idx >= 0)
{
sb.Append(original, last, idx - last);
sb.Append(newValue);
last = idx + oldValue.Length;
idx = original.IndexOf(oldValue, last, comparisonType);
}
sb.Append(original, last, original.Length - last);
return sb.ToString();
}
private static string GetCellText(Cell cell, List<string> allSharedStrings)
{
if (cell?.CellValue is null) return string.Empty;
if (cell.InlineString is not null)
return string.Concat(cell.InlineString.Descendants<Text>().Select(t => t.Text));
string value = cell.CellValue.InnerText;
if (cell.DataType?.Value == CellValues.SharedString)
{
if (int.TryParse(value, out int idx) && idx >= 0 && idx < allSharedStrings.Count)
return allSharedStrings[idx];
return string.Empty;
}
return value;
}
private static int AddOrFindStringIndex(List<string> allSharedStrings, string text)
{
int idx = allSharedStrings.IndexOf(text);
if (idx >= 0) return idx;
allSharedStrings.Add(text);
return allSharedStrings.Count - 1;
}
// --- Колонтитулы (обобщённый метод) ---
private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
var worksheet = worksheetPart.Worksheet;
if (worksheet is null) return;
var headerFooter = worksheet.Descendants<HeaderFooter>().FirstOrDefault();
if (headerFooter is null) return;
foreach (var elem in new OpenXmlLeafTextElement?[] { headerFooter.OddHeader, headerFooter.OddFooter, headerFooter.EvenHeader, headerFooter.EvenFooter, headerFooter.FirstHeader, headerFooter.FirstFooter })
ReplaceHeaderFooter(elem, replacementDict, comparisonType);
}
private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
if (element?.Text is null) return;
string original = element.Text;
string processed = ProcessReplacements(original, replacementDict, comparisonType);
if (processed != original)
element.Text = processed;
}
// --- Комментарии ---
private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
{
var commentsPart = worksheetPart.WorksheetCommentsPart;
if (commentsPart?.Comments is null) return;
foreach (var comment in commentsPart.Comments.Elements<Comment>())
{
var textElement = comment.Descendants<CommentText>().FirstOrDefault();
if (textElement?.Text is null) continue;
string original = textElement.Text.Text;
if (string.IsNullOrEmpty(original)) continue;
string processed = ProcessReplacements(original, replacementDict, comparisonType);
if (processed != original)
textElement.Text.Text = processed;
}
}
}

View File

@@ -0,0 +1,12 @@
global using DocumentFormat.OpenXml;
global using DocumentFormat.OpenXml.Packaging;
global using DocumentFormat.OpenXml.Spreadsheet;
global using System;
global using System.Collections.Generic;
global using System.Diagnostics;
global using System.Globalization;
global using System.IO;
global using System.Linq;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;