using System.Globalization; namespace QWERTYkez.ExcelProcessor; internal static class ReplaceNumericExtensions { // =========================== МНОЖЕСТВЕННЫЕ ЗАМЕНЫ =========================== // --- Double --- internal static void Replace(this SpreadsheetDocument doc, IEnumerable> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase) { if (replacements is null) return; WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!]; if (worksheets.Length < 1) return; var coll = replacements as ICollection>; int count = coll?.Count ?? replacements.Count(); if (count == 0) return; var workbookPart = doc.WorkbookPart!; var stringReplacements = new Dictionary(count); foreach (var kvp in replacements) stringReplacements[kvp.Key] = kvp.Value.ToString(format ?? "G", CultureInfo.CurrentCulture); ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, format, comparisonType, (val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture)); } // --- Float --- internal static void Replace(this SpreadsheetDocument doc, IEnumerable> replacements, string? format = null, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase) { if (replacements is null) return; WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!]; if (worksheets.Length < 1) return; var coll = replacements as ICollection>; int count = coll?.Count ?? replacements.Count(); if (count == 0) return; var workbookPart = doc.WorkbookPart!; var stringReplacements = new Dictionary(count); foreach (var kvp in replacements) stringReplacements[kvp.Key] = kvp.Value.ToString(format ?? "G", CultureInfo.CurrentCulture); ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, format, comparisonType, (val, fmt) => val.ToString(fmt ?? "G", CultureInfo.InvariantCulture)); } // --- Int --- internal static void Replace(this SpreadsheetDocument doc, IEnumerable> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase) { if (replacements is null) return; WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!]; if (worksheets.Length < 1) return; var coll = replacements as ICollection>; int count = coll?.Count ?? replacements.Count(); if (count == 0) return; var workbookPart = doc.WorkbookPart!; var stringReplacements = new Dictionary(count); foreach (var kvp in replacements) stringReplacements[kvp.Key] = kvp.Value.ToString(CultureInfo.CurrentCulture); ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, null, comparisonType, (val, _) => val.ToString(CultureInfo.InvariantCulture)); } // --- Long --- internal static void Replace(this SpreadsheetDocument doc, IEnumerable> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase) { if (replacements is null) return; WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!]; if (worksheets.Length < 1) return; var coll = replacements as ICollection>; int count = coll?.Count ?? replacements.Count(); if (count == 0) return; var workbookPart = doc.WorkbookPart!; var stringReplacements = new Dictionary(count); foreach (var kvp in replacements) stringReplacements[kvp.Key] = kvp.Value.ToString(CultureInfo.CurrentCulture); ReplaceNumericCore(workbookPart, worksheets, replacements, stringReplacements, null, comparisonType, (val, _) => val.ToString(CultureInfo.InvariantCulture)); } // =========================== ОДИНОЧНЫЕ ЗАМЕНЫ =========================== // --- 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( WorkbookPart workbookPart, WorksheetPart[] worksheets, IEnumerable> numericReplacements, Dictionary stringReplacements, string? format, StringComparison comparisonType, Func numberToStringForNumberCell) { var formatCache = new Dictionary(); 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(); var sharedStringIndexMap = new Dictionary(); var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable; if (sharedStringTable != null) { foreach (var item in sharedStringTable.Elements()) { var text = ConcatTexts(item.Descendants()); sharedStringIndexMap[text] = allSharedStrings.Count; allSharedStrings.Add(text); } } foreach (var worksheetPart in worksheets) { var worksheet = worksheetPart.Worksheet; if (worksheet == null) continue; var sheetData = worksheet.GetFirstChild(); if (sheetData == null) continue; // Обработка ячеек foreach (var row in sheetData.Elements()) { foreach (var cell in row.Elements()) { 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( WorkbookPart workbookPart, WorksheetPart[] worksheets, string oldValue, T newValue, string? format, StringComparison comparisonType, Func numberToStringForNumberCell, Func numberToStringForTextCell) { var formatCache = new Dictionary(); 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 { { oldValue, numberToStringForTextCell(newValue, format) } }; // Инициализация SharedStringTable var allSharedStrings = new List(); var sharedStringIndexMap = new Dictionary(); var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable; if (sharedStringTable != null) { foreach (var item in sharedStringTable.Elements()) { var text = ConcatTexts(item.Descendants()); sharedStringIndexMap[text] = allSharedStrings.Count; allSharedStrings.Add(text); } } foreach (var worksheetPart in worksheets) { var worksheet = worksheetPart.Worksheet; if (worksheet == null) continue; var sheetData = worksheet.GetFirstChild(); if (sheetData == null) continue; foreach (var row in sheetData.Elements()) { foreach (var cell in row.Elements()) { 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 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()) 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 allSharedStrings, Dictionary sharedStringIndexMap) { if (cell.InlineString != null) { // Очищаем старые тексты foreach (var t in cell.InlineString.Descendants().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 allSharedStrings) { var ssPart = workbookPart.SharedStringTablePart; ssPart ??= workbookPart.AddNewPart(); var sharedStringTable = ssPart.SharedStringTable ?? new SharedStringTable(); sharedStringTable.RemoveAllChildren(); foreach (var str in allSharedStrings) sharedStringTable.AppendChild(new SharedStringItem(new Text(str))); sharedStringTable.Save(); } private static string ConcatTexts(IEnumerable 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(); stylesPart.Stylesheet = new Stylesheet(); } var ss = stylesPart.Stylesheet!; ss.NumberingFormats ??= new NumberingFormats(); uint nextId = 164; if (ss.NumberingFormats.Elements().Any()) nextId = ss.NumberingFormats.Elements().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 replacementDict, StringComparison comparisonType) { var worksheet = worksheetPart.Worksheet; if (worksheet is null) return; var headerFooter = worksheet.Descendants().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 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 replacementDict, StringComparison comparisonType) { var commentsPart = worksheetPart.WorksheetCommentsPart; if (commentsPart?.Comments is null) return; foreach (var comment in commentsPart.Comments.Elements()) { var textElement = comment.Descendants().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 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(); } }