From 8d5ee560812dd3304ea6f423b974a84e6d454e93 Mon Sep 17 00:00:00 2001 From: upstream Date: Wed, 2 Sep 2026 11:15:38 +0200 Subject: [PATCH] Upstream ClosedXML/ClosedXML state of the touched files before f8af6b544a --- .../ConditionalFormatTests.cs | 233 ++++++++++++++++++ .../ConditionalFormatting/CFDataBar.xlsx | Bin 0 -> 6634 bytes .../CFDataBarNegative.xlsx | Bin 0 -> 6749 bytes .../ConditionalFormatting/CFDataBars.xlsx | Bin 0 -> 7278 bytes .../Save/XLCFDataBarConverterExtension.cs | 82 ++++++ 5 files changed, 315 insertions(+) create mode 100644 ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs create mode 100644 ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx create mode 100644 ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx create mode 100644 ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx create mode 100644 ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs diff --git a/ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs b/ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs new file mode 100644 index 0000000..d63198c --- /dev/null +++ b/ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs @@ -0,0 +1,233 @@ +using System; +using ClosedXML.Excel; +using NUnit.Framework; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; + +namespace ClosedXML.Tests.Excel.ConditionalFormats +{ + [TestFixture] + public class ConditionalFormatTests + { + [Test] + public void MaintainConditionalFormattingOrder() + { + // The input file contains duplicates of same dxf and thus the output also contains some duplicate dxf. + TestHelper.LoadSaveAndCompare( + @"Other\StyleReferenceFiles\ConditionalFormattingOrder\inputfile.xlsx", + @"Other\StyleReferenceFiles\ConditionalFormattingOrder\ConditionalFormattingOrder.xlsx"); + } + + [TestCase(true, 7)] + [TestCase(false, 8)] + public void SaveOptionAffectsConsolidationConditionalFormatRanges(bool consolidateConditionalFormatRanges, int expectedCount) + { + var options = new SaveOptions + { + ConsolidateConditionalFormatRanges = consolidateConditionalFormatRanges + }; + + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet"); + + ws.Range("D2:D3").AddConditionalFormat().DataBar(XLColor.Red).LowestValue().HighestValue(); + ws.Range("B2:B3").AddConditionalFormat().DataBar(XLColor.Red).LowestValue().HighestValue(); + ws.Range("E2:E6").AddConditionalFormat().ColorScale().LowestValue(XLColor.Red).HighestValue(XLColor.Blue); + ws.Range("F2:F6").AddConditionalFormat().ColorScale().LowestValue(XLColor.Red).HighestValue(XLColor.Blue); + ws.Range("G2:G7").AddConditionalFormat().WhenIsUnique().Fill.SetBackgroundColor(XLColor.Blue); + ws.Range("H2:H7").AddConditionalFormat().WhenIsUnique().Fill.SetBackgroundColor(XLColor.Blue); + ws.Range("I2:I6").AddConditionalFormat().WhenContains("test"); + ws.Range("J2:J6").AddConditionalFormat().WhenContains("test"); + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, options); + var wb_saved = new XLWorkbook(ms); + Assert.AreEqual(expectedCount, wb_saved.Worksheet("Sheet").ConditionalFormats.Count()); + } + } + + [TestCase(true, 1)] + [TestCase(false, 2)] + public void SaveOptionAffectsConsolidationDataValidationRanges(bool consolidateDataValidationRanges, int expectedCount) + { + var options = new SaveOptions + { + ConsolidateDataValidationRanges = consolidateDataValidationRanges + }; + + var wb = new XLWorkbook(); + var ws = wb.AddWorksheet("Sheet"); + ws.Range("C2:C5").CreateDataValidation().Decimal.Between(1, 5); + ws.Range("D2:D5").CreateDataValidation().Decimal.Between(1, 5); + + using (var ms = new MemoryStream()) + { + wb.SaveAs(ms, options); + var wb_saved = new XLWorkbook(ms); + Assert.AreEqual(expectedCount, wb_saved.Worksheet("Sheet").DataValidations.Count()); + } + } + + [TestCase("en-US")] + [TestCase("fr-FR")] + [TestCase("ru-RU")] + public void SaveConditionalFormat_CultureIndependent(string culture) + { + using (var ms = new MemoryStream()) + { + var expectedValue = 1.5; + Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture); + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet(); + var i = 1; + ws.Cell(i++, 1).AddConditionalFormat().WhenEquals(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenNotEquals(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenGreaterThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenLessThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenEqualOrGreaterThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenEqualOrLessThan(expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenBetween(expectedValue, expectedValue).Fill.SetBackgroundColor(XLColor.Red); + ws.Cell(i++, 1).AddConditionalFormat().WhenNotBetween(expectedValue, expectedValue).Fill.SetBackgroundColor(XLColor.Red); + + wb.SaveAs(ms); + } + + using (var wb = new XLWorkbook(ms)) + { + var ws = wb.Worksheets.First(); + + var conditionalFormatValues = ws.ConditionalFormats + .SelectMany(cf => cf.Values.Values) + .Select(v => v.Value) + .Distinct(); + + Assert.AreEqual(1, conditionalFormatValues.Count()); + Assert.AreEqual("1.5", conditionalFormatValues.Single()); + } + } + } + + [Test] + public void CellIs_type_reads_only_required_formula_arguments() + { + // The CellIs uses formula tags as arguments. Some producers generate extra empty + // formula tags and ClosedXml should be able to load CellIs conditional formatting + // with such extra tags without an exception. The test file has been modified to + // include extra formula tags and test checks that extra tags are ignored. + TestHelper.LoadAndAssert((_, ws) => + { + AssertFormulaArgs(ws, XLCFOperator.Between, "$D$2", "$E$2"); + AssertFormulaArgs(ws, XLCFOperator.NotBetween, "$D$3", "$E$3"); + AssertFormulaArgs(ws, XLCFOperator.GreaterThan, "$D$4"); + AssertFormulaArgs(ws, XLCFOperator.LessThan, "$D$5"); + AssertFormulaArgs(ws, XLCFOperator.Equal, "$D$6"); + }, @"Other\ConditionalFormats\Extra_formulas_CellIs_type.xlsx"); + + static void AssertFormulaArgs(IXLWorksheet ws, XLCFOperator cfOperator, params string[] expectedFormulas) + { + var cf = ws.ConditionalFormats.Single(cf => cf.ConditionalFormatType == XLConditionalFormatType.CellIs && cf.Operator == cfOperator); + Assert.AreEqual(expectedFormulas.Length, cf.Values.Count); + CollectionAssert.AreEqual(expectedFormulas, cf.Values.Select(v => v.Value.Value)); + } + } + + [Test] + public void Expression_type_skips_empty_formula_tags() + { + // The Expression uses formula tag as arguments. Some producers generate extra empty + // formula tags and ClosedXml should be able to load Expression conditional formatting + // with such extra tags without an exception. The test file has been modified to + // include extra formula tags and test checks that extra tags are ignored. + TestHelper.LoadAndAssert((_, ws) => + { + AssertFormulaArgs(ws, "A1:A1", "$C$1=5"); + AssertFormulaArgs(ws, "A2:A2", "$C$2=4"); + }, @"Other\ConditionalFormats\Extra_formulas_Expression_type.xlsx"); + + static void AssertFormulaArgs(IXLWorksheet ws, string range, string expectedFormula) + { + var cf = ws.ConditionalFormats.Single(cf => cf.ConditionalFormatType == XLConditionalFormatType.Expression && cf.Range.RangeAddress.ToString() == range); + Assert.AreEqual(1, cf.Values.Count); + CollectionAssert.AreEqual(expectedFormula, cf.Values[1].Value); + } + } + + [Test] + public void Range_setter_throws_on_range_from_different_worksheet() + { + using var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet(); + var cf = ws1.Range("A1").AddConditionalFormat(); + var ws2 = wb.AddWorksheet(); + var differentSheetRange = ws2.Range("B5"); + + Assert.Throws(() => cf.Range = differentSheetRange); + } + + [Test] + public void Ranges_setter_throws_on_range_from_different_worksheet() + { + using var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet(); + var cf = ws1.Range("A1").AddConditionalFormat(); + + var ranges = ws1.Ranges("B1"); + var differentSheetRange = wb.AddWorksheet().Range("C1"); + ranges.Add(differentSheetRange); + + Assert.That(() => cf.Ranges = ranges, Throws.TypeOf().With.Message.Contains("must be from worksheet")); + } + + [Test] + public void Ranges_setter_throws_on_empty_ranges() + { + using var wb = new XLWorkbook(); + var ws1 = wb.AddWorksheet(); + var cf = ws1.Range("A1").AddConditionalFormat(); + + var emptyRanges = ws1.Ranges(""); + + Assert.That(() => cf.Ranges = emptyRanges, Throws.TypeOf().With.Message.Contains("empty")); + } + + [Test] + public void Text_formats_are_loaded_as_value() + { + // Issue #2690: Ensure that CF for text are loaded as a value and not as a formula. + TestHelper.CreateSaveLoadAssert( + (_, ws) => + { + ws.AddConditionalFormat().WhenContains("Contains") + .Fill.SetBackgroundColor(XLColor.Red); + + ws.AddConditionalFormat().WhenNotContains("Not Contains") + .Fill.SetBackgroundColor(XLColor.Green); + + ws.AddConditionalFormat().WhenStartsWith("Starts With") + .Fill.SetBackgroundColor(XLColor.Blue); + + ws.AddConditionalFormat().WhenEndsWith("Ends With") + .Fill.SetBackgroundColor(XLColor.Black); + }, + (_, ws) => + { + AssertTextCfValue(ws, XLConditionalFormatType.ContainsText, "Contains"); + AssertTextCfValue(ws, XLConditionalFormatType.NotContainsText, "Not Contains"); + AssertTextCfValue(ws, XLConditionalFormatType.StartsWith, "Starts With"); + AssertTextCfValue(ws, XLConditionalFormatType.EndsWith, "Ends With"); + }); + return; + + static void AssertTextCfValue(IXLWorksheet ws, XLConditionalFormatType type, string text) + { + var cf = ws.ConditionalFormats.Single(x => x.ConditionalFormatType == type); + Assert.AreEqual(1, cf.Values.Count); + Assert.AreEqual(text, cf.Values[1].Value); + Assert.IsFalse(cf.Values[1].IsFormula); + } + } + } +} diff --git a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..fe7d0f22fbaee1189c8d0ba8b29a70e5860cef72 GIT binary patch literal 6634 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 literal 0 HcmV?d00001 diff --git a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..afae098d1cc604822851e2979560c403aae6ee43 GIT binary patch literal 6749 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 KnM86Poc$Lhk{mPu literal 0 HcmV?d00001 diff --git a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..236b6e30e20ea7718dd6e8853c57e5be8a200932 GIT binary patch literal 7278 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 literal 0 HcmV?d00001 diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs new file mode 100644 index 0000000..0ea59f6 --- /dev/null +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs @@ -0,0 +1,82 @@ +using ClosedXML.Extensions; +using DocumentFormat.OpenXml.Office.Excel; +using DocumentFormat.OpenXml.Office2010.Excel; +using System.Collections.Generic; +using System.Linq; + +namespace ClosedXML.Excel +{ + internal class XLCFDataBarConverterExtension : IXLCFConverterExtension + { + private static readonly IReadOnlyDictionary CFValueToTypeMap = + new Dictionary + { + { DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Max, ConditionalFormattingValueObjectTypeValues.AutoMax }, + { DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Min, ConditionalFormattingValueObjectTypeValues.AutoMin }, + { DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Number, ConditionalFormattingValueObjectTypeValues.Numeric }, + { DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Percent, ConditionalFormattingValueObjectTypeValues.Percent }, + { DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Percentile, ConditionalFormattingValueObjectTypeValues.Percentile }, + { DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Formula, ConditionalFormattingValueObjectTypeValues.Formula }, + }; + + public ConditionalFormattingRule Convert(IXLConditionalFormat cf, XLWorkbook.SaveContext context) + { + ConditionalFormattingRule conditionalFormattingRule = new ConditionalFormattingRule() + { + Type = DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValues.DataBar, + Id = ((XLConditionalFormat)cf).Id.WrapInBraces() + }; + + DataBar dataBar = new DataBar() + { + MinLength = 0, + MaxLength = 100, + Gradient = true, + ShowValue = !cf.ShowBarOnly + }; + + var cfMinType = cf.ContentTypes.TryGetValue(1, out var contentType1) + ? GetCFType(contentType1.ToOpenXml()) + : ConditionalFormattingValueObjectTypeValues.AutoMin; + var cfMin = new ConditionalFormattingValueObject { Type = cfMinType }; + if (cf.Values.Any() && cf.Values[1]?.Value != null) + { + cfMin.Type = ConditionalFormattingValueObjectTypeValues.Numeric; + cfMin.Append(new Formula() { Text = cf.Values[1].Value }); + } + + var cfMaxType = cf.ContentTypes.TryGetValue(2, out var contentType2) + ? GetCFType(contentType2.ToOpenXml()) + : ConditionalFormattingValueObjectTypeValues.AutoMax; + var cfMax = new ConditionalFormattingValueObject { Type = cfMaxType }; + if (cf.Values.Count >= 2 && cf.Values[2]?.Value != null) + { + cfMax.Type = ConditionalFormattingValueObjectTypeValues.Numeric; + cfMax.Append(new Formula() { Text = cf.Values[2].Value }); + } + + var barAxisColor = new BarAxisColor { Rgb = XLColor.Black.Color.ToHex() }; + + var negativeFillColor = new NegativeFillColor { Rgb = cf.Colors[1].Color.ToHex() }; + if (cf.Colors.Count == 2) + { + negativeFillColor = new NegativeFillColor { Rgb = cf.Colors[2].Color.ToHex() }; + } + + dataBar.Append(cfMin); + dataBar.Append(cfMax); + + dataBar.Append(negativeFillColor); + dataBar.Append(barAxisColor); + + conditionalFormattingRule.Append(dataBar); + + return conditionalFormattingRule; + } + + private static ConditionalFormattingValueObjectTypeValues GetCFType(DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues value) + { + return CFValueToTypeMap[value]; + } + } +}