Skip to content

Commit 728c829

Browse files
authored
Merge pull request #22 from skbkontur/l.rybin/Add-custom-cell-naming
2 parents 1c2fe21 + 4af40ce commit 728c829

File tree

13 files changed

+345
-4
lines changed

13 files changed

+345
-4
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System.Collections.Generic;
2+
3+
using DocumentFormat.OpenXml.Spreadsheet;
4+
5+
using FluentAssertions;
6+
7+
using NUnit.Framework;
8+
9+
using SkbKontur.Excel.TemplateEngine.FileGenerating.Helpers;
10+
11+
namespace SkbKontur.Excel.TemplateEngine.Tests.DefinedNamesTests
12+
{
13+
public class DefinedNameHelperTests
14+
{
15+
[TestCaseSource(nameof(GetIsBelongsToSheetTestData))]
16+
public void IsBelongsToSheetTest_ShouldBelong(string definedNameInnerText, string sheetName)
17+
{
18+
var definedName = new DefinedName(definedNameInnerText);
19+
var sheet = new Sheet {Name = sheetName};
20+
21+
definedName.BelongsToSheet(sheet).Should().BeTrue();
22+
}
23+
24+
[TestCase("Лист1!$A$1", "Лист 1")]
25+
[TestCase("'Лист 1'!$A$1", "Лист1")]
26+
public void IsBelongsToSheetTest_ShouldNotBelong(string definedNameInnerText, string sheetName)
27+
{
28+
var definedName = new DefinedName(definedNameInnerText);
29+
var sheet = new Sheet {Name = sheetName};
30+
31+
definedName.BelongsToSheet(sheet).Should().BeFalse();
32+
}
33+
34+
[TestCaseSource(nameof(GetReplaceSheetNameInFormulaTestData))]
35+
public void ReplaceSheetNameInFormulaTest(string innerText, string name, string expectedDefinedNameInnerText)
36+
{
37+
var definedName = new DefinedName(innerText);
38+
39+
var updatedDefinedName = definedName.ReplaceSheetNameInFormula(name);
40+
updatedDefinedName.InnerText.Should().Be(expectedDefinedNameInnerText);
41+
}
42+
43+
private static IEnumerable<TestCaseData> GetIsBelongsToSheetTestData()
44+
{
45+
return new[]
46+
{
47+
new TestCaseData("Лист1!$A$1", "Лист1"),
48+
new TestCaseData("Лист_1!$A$1", "Лист_1"),
49+
new TestCaseData("'Лист''1'!$A$1", "Лист'1"),
50+
new TestCaseData("'Лист 1'!$A$1", "Лист 1"),
51+
new TestCaseData("'Лист1!'!$A$1", "Лист1!"),
52+
new TestCaseData("'Лист1 !'!$A$1", "Лист1 !"),
53+
new TestCaseData("'Лист1 ''!'!$A$1", "Лист1 '!"),
54+
new TestCaseData("' Лист1 '!$A$1", " Лист1 "),
55+
new TestCaseData("Лист1!$F$18:$F$201", "Лист1"),
56+
new TestCaseData("' Лист1 ''!'!$F$18:$F$201", " Лист1 '!")
57+
};
58+
}
59+
60+
private static IEnumerable<TestCaseData> GetReplaceSheetNameInFormulaTestData()
61+
{
62+
return new[]
63+
{
64+
new TestCaseData("Лист1!$A$1", "Лист2", "Лист2!$A$1"),
65+
66+
// Пробел
67+
new TestCaseData("Лист1!$A$1", "Лист 1", "'Лист 1'!$A$1"),
68+
69+
// Спецсимвол
70+
new TestCaseData("Лист1!$A$1", "Лист_1", "Лист_1!$A$1"),
71+
new TestCaseData("Лист1!$A$1", "_Лист1", "_Лист1!$A$1"),
72+
new TestCaseData("Лист1!$A$1", "Лист1!", "'Лист1!'!$A$1"),
73+
new TestCaseData("Лист1!$A$1", "Ли%ст1", "'Ли%ст1'!$A$1"),
74+
new TestCaseData("Лист1!$A$1", "$Лист1$", "'$Лист1$'!$A$1"),
75+
76+
// Первый символ - цифра
77+
new TestCaseData("Лист1!$A$1", "1Лист", "'1Лист'!$A$1"),
78+
new TestCaseData("Лист1!$A$1", "034Лист", "'034Лист'!$A$1"),
79+
80+
// Одинарная кавычка
81+
new TestCaseData("Лист1!$A$1", "Лист'1", "'Лист''1'!$A$1"),
82+
83+
// A1-нотация
84+
new TestCaseData("Лист1!$A$1", "A1", "'A1'!$A$1"),
85+
new TestCaseData("Лист1!$A$1", "a1", "'a1'!$A$1"),
86+
new TestCaseData("Лист1!$A$1", "ACB12", "'ACB12'!$A$1"),
87+
new TestCaseData("Лист1!$A$1", "ACb12", "'ACb12'!$A$1")
88+
};
89+
}
90+
}
91+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System.IO;
2+
using System.Text;
3+
4+
using NUnit.Framework;
5+
6+
using FluentAssertions;
7+
8+
using SkbKontur.Excel.TemplateEngine.FileGenerating;
9+
using SkbKontur.Excel.TemplateEngine.ObjectPrinting.ExcelDocumentPrimitives.Implementations;
10+
using SkbKontur.Excel.TemplateEngine.ObjectPrinting.NavigationPrimitives.Implementations;
11+
using SkbKontur.Excel.TemplateEngine.ObjectPrinting.TableBuilder;
12+
using SkbKontur.Excel.TemplateEngine.ObjectPrinting.TableNavigator;
13+
14+
using Vostok.Logging.Console;
15+
16+
namespace SkbKontur.Excel.TemplateEngine.Tests.ObjectPrintingTests
17+
{
18+
public class DefinedNamesTests : FileBasedTestBase
19+
{
20+
[Test]
21+
public void TestCopyDefinedNames()
22+
{
23+
using (var templateDocument = ExcelDocumentFactory.CreateFromTemplate(File.ReadAllBytes(GetFilePath("customCellNames.xlsx")), logger))
24+
using (var targetDocument = ExcelDocumentFactory.CreateFromTemplate(File.ReadAllBytes(GetFilePath("empty.xlsm")), logger))
25+
{
26+
targetDocument.CopyDefinedNamesFrom(templateDocument);
27+
28+
var template = new ExcelTable(templateDocument.GetWorksheet(0));
29+
var templateEngine = new TemplateEngine(template, logger);
30+
31+
var target = new ExcelTable(targetDocument.GetWorksheet(0));
32+
var tableNavigator = new TableNavigator(new CellPosition("A1"), logger);
33+
var tableBuilder = new TableBuilder(target, tableNavigator, new Style(template.GetCell(new CellPosition("A1"))));
34+
templateEngine.Render(tableBuilder, new {Test = "b"});
35+
36+
var expectedData = ExcelDocumentFactory.CreateFromTemplate(File.ReadAllBytes(GetFilePath("customCellNamesResult.xlsm")), logger);
37+
var actualData = ExcelDocumentFactory.CreateFromTemplate(targetDocument.CloseAndGetDocumentBytes(), logger);
38+
39+
var expectedDefinedNames = expectedData.GetDefinedNames().InnerXml;
40+
var actualDefinedNames = actualData.GetDefinedNames().InnerXml;
41+
42+
expectedDefinedNames.Should().Match(actualDefinedNames);
43+
44+
var expectedString = expectedData.ToString();
45+
var actualString = actualData.ToString();
46+
47+
var expectedBytes = Encoding.Default.GetBytes(expectedString);
48+
var actualBytes = Encoding.Default.GetBytes(actualString);
49+
50+
actualBytes.Should().Equal(expectedBytes);
51+
}
52+
}
53+
54+
/// <summary>
55+
/// Проверка корректности элементов DefinedNames после переименования листа
56+
/// </summary>
57+
[Test]
58+
public void TestDefinedNamesUpdatedAfterSheetRename()
59+
{
60+
using (var document = ExcelDocumentFactory
61+
.CreateFromTemplate(File.ReadAllBytes(GetFilePath("definedNamesUpdatedAfterSheetRename.xlsx")),
62+
logger))
63+
{
64+
document.RenameWorksheet(index : 0, name : "НазваниеБезПробелов", updateDefinedNames : true);
65+
document.RenameWorksheet(index : 1, name : "НазваниеС Пробелом", updateDefinedNames : true);
66+
document.RenameWorksheet(index : 2, name : "НазваниеСо_Спецсимволом", updateDefinedNames : true);
67+
document.RenameWorksheet(index : 3, name : "НазваниеСо+Спецсимволом", updateDefinedNames : true);
68+
document.RenameWorksheet(index : 4, name : "1НазваниеСЦифройВначале", updateDefinedNames : true);
69+
document.RenameWorksheet(index : 5, name : "НазваниеС'ОдинарнойКовычкой", updateDefinedNames : true);
70+
document.RenameWorksheet(index : 6, name : "AN1", updateDefinedNames : true);
71+
72+
var expectedData = ExcelDocumentFactory.CreateFromTemplate(
73+
File.ReadAllBytes(GetFilePath("definedNamesUpdatedAfterSheetRenameResult.xlsx")),
74+
logger);
75+
var actualData = ExcelDocumentFactory.CreateFromTemplate(
76+
document.CloseAndGetDocumentBytes(),
77+
logger);
78+
79+
var expectedDefinedNames = expectedData.GetDefinedNames().InnerXml;
80+
var actualDefinedNames = actualData.GetDefinedNames().InnerXml;
81+
82+
expectedDefinedNames.Should().Match(actualDefinedNames);
83+
}
84+
}
85+
86+
private readonly ConsoleLog logger = new ConsoleLog();
87+
}
88+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Excel.TemplateEngine.Tests/ObjectPrintingTests/ManualPrintingTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ public void TestPrintingDropDownFromTheOtherWorksheet()
4545
}
4646
}
4747

48+
[Test]
49+
public void TestCopyCustomCellNames()
50+
{
51+
using (var templateDocument = ExcelDocumentFactory.CreateFromTemplate(File.ReadAllBytes(GetFilePath("customCellNames_manual.xlsx")), logger))
52+
using (var targetDocument = ExcelDocumentFactory.CreateFromTemplate(File.ReadAllBytes(GetFilePath("empty.xlsm")), logger))
53+
{
54+
targetDocument.CopyDefinedNamesFrom(templateDocument);
55+
56+
var filename = "output.xlsm";
57+
File.WriteAllBytes(filename, targetDocument.CloseAndGetDocumentBytes());
58+
59+
var path = "file:///" + Path.GetFullPath(filename).Replace("\\", "/");
60+
Assert.Fail($"Please manually open file '{path}' and check that cells B2 and D4 have names 'Custom_Name'");
61+
}
62+
}
63+
4864
[Test]
4965
public void TestPrintingVbaMacros()
5066
{
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
using System.Text.RegularExpressions;
3+
4+
using DocumentFormat.OpenXml.Spreadsheet;
5+
6+
namespace SkbKontur.Excel.TemplateEngine.FileGenerating.Helpers;
7+
8+
internal static class DefinedNameHelper
9+
{
10+
public static bool BelongsToSheet(this DefinedName definedName, Sheet sheet)
11+
{
12+
if (string.IsNullOrEmpty(definedName.InnerText))
13+
return false;
14+
15+
var separatorIndex = definedName.InnerText.LastIndexOf(referencePartSeparator, StringComparison.Ordinal);
16+
var sheetNamePart = definedName.InnerText.Substring(0, separatorIndex);
17+
18+
var escapedSheetName = sheet.Name?.Value?.Replace("'", "''");
19+
var isBelong = sheetNamePart.Contains($"'{escapedSheetName}'") || sheetNamePart.Contains($"{escapedSheetName}");
20+
21+
return isBelong;
22+
}
23+
24+
public static DefinedName ReplaceSheetNameInFormula(this DefinedName definedName, string newSheetName)
25+
{
26+
if (string.IsNullOrEmpty(newSheetName))
27+
throw new InvalidOperationException("Sheet name cannot be null or empty");
28+
29+
var newSheetNamePart = FormatWorksheetNameForReference(newSheetName);
30+
31+
var separatorIndex = definedName.InnerText.LastIndexOf(referencePartSeparator, StringComparison.Ordinal);
32+
var cellAddress = definedName.InnerText.Substring(separatorIndex + 1);
33+
34+
var newInnerText = $"{newSheetNamePart}{referencePartSeparator}{cellAddress}";
35+
36+
return new DefinedName(newInnerText) {Name = definedName.Name};
37+
}
38+
39+
private static string FormatWorksheetNameForReference(string name)
40+
{
41+
if (NeedsSingleQuotes(name))
42+
{
43+
var escapedName = name.Replace("'", "''");
44+
return $"'{escapedName}'";
45+
}
46+
47+
return name;
48+
}
49+
50+
private static bool NeedsSingleQuotes(string name)
51+
{
52+
if (name.Contains(" "))
53+
return true;
54+
55+
if (char.IsDigit(name[0]))
56+
return true;
57+
58+
// Специальный символ (кроме нижнего подчеркивания)
59+
const string specialCharacterPattern = @"[^\p{L}\p{N}\s_]";
60+
if (Regex.IsMatch(name, specialCharacterPattern))
61+
return true;
62+
63+
// A1-нотация: сперва английские буквы, затем цифры
64+
const string a1NotationPattern = @"^[A-Za-z]+[\d]+$";
65+
if (Regex.IsMatch(name, a1NotationPattern))
66+
return true;
67+
68+
return false;
69+
}
70+
71+
// Символ-разделитель в ссылках, отделяющий имя листа от диапазона ячеек (например, 'Рабочий лист'!B2:C10)
72+
private const string referencePartSeparator = "!";
73+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#nullable enable
2+
3+
using DocumentFormat.OpenXml.Spreadsheet;
4+
5+
namespace SkbKontur.Excel.TemplateEngine.FileGenerating.Helpers;
6+
7+
internal static class DefinedNamesHelper
8+
{
9+
public static DefinedNames UpdateAfterRenamingSheet(this DefinedNames definedNames,
10+
Sheet sheetBeforeRenaming,
11+
string newName)
12+
{
13+
var updatedDefinedNames = new DefinedNames();
14+
foreach (var definedName in definedNames.Elements<DefinedName>())
15+
{
16+
if (definedName.BelongsToSheet(sheetBeforeRenaming))
17+
{
18+
var updatedDefinedName = definedName.ReplaceSheetNameInFormula(newName);
19+
updatedDefinedNames.AppendChild(updatedDefinedName.CloneNode(deep : true));
20+
}
21+
else
22+
updatedDefinedNames.AppendChild(definedName.CloneNode(deep : true));
23+
}
24+
25+
return updatedDefinedNames;
26+
}
27+
}

0 commit comments

Comments
 (0)