using System; using ClosedXML.Excel; using NUnit.Framework; using System.Globalization; using System.IO; using System.IO.Packaging; using System.Linq; using System.Threading; using System.Xml.Linq; using SaveOptions = ClosedXML.Excel.SaveOptions; namespace ClosedXML.Tests.Excel.ConditionalFormats { [TestFixture] public class ConditionalFormatTests { [Test] public void DataBarLowestHighestValueWritesMinMaxCfvoWithoutFormula() { // https://github.com/ClosedXML/ClosedXML/issues/2843 // DataBar().LowestValue()/HighestValue() must emit the x14 cfvo nodes as // type="min"/type="max" without an xm:f formula. The regression wrote them // as type="num" locked to 0, which strict parsers (e.g. WPS) render wrong. XNamespace x14 = "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"; using var ms = new MemoryStream(); using (var wb = new XLWorkbook()) { var ws = wb.AddWorksheet("Sheet1"); ws.Cell("A1").Value = 1; ws.Cell("A2").Value = -1; ws.Cell("A3").Value = 2; ws.Range("A1:A3").AddConditionalFormat() .DataBar(XLColor.Red) .LowestValue() .HighestValue(); wb.SaveAs(ms); } ms.Position = 0; using var package = Package.Open(ms, FileMode.Open, FileAccess.Read); var part = package.GetPart(new Uri("/xl/worksheets/sheet1.xml", UriKind.Relative)); using var partStream = part.GetStream(); var doc = XDocument.Load(partStream); var x14DataBar = doc.Descendants(x14 + "dataBar").Single(); var cfvos = x14DataBar.Elements(x14 + "cfvo").ToList(); Assert.AreEqual(2, cfvos.Count); Assert.AreEqual("min", cfvos[0].Attribute("type")?.Value); Assert.AreEqual("max", cfvos[1].Attribute("type")?.Value); Assert.IsFalse( x14DataBar.Descendants().Any(e => e.Name.LocalName == "f"), "x14 cfvo for lowest/highest value must not contain a formula."); } [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); } } } }