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

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,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();
}
}