diff --git a/ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs b/ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs index 3c98963..d63198c 100644 --- a/ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs +++ b/ClosedXML.Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs @@ -3,57 +3,14 @@ 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() { diff --git a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx index b2ded29..fe7d0f2 100644 Binary files a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx and b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBar.xlsx differ diff --git a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx index 5b4b614..afae098 100644 Binary files a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx and b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBarNegative.xlsx differ diff --git a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx index 24ed742..236b6e3 100644 Binary files a/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx and b/ClosedXML.Tests/Resource/Examples/ConditionalFormatting/CFDataBars.xlsx differ diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs index 1e97c40..0ea59f6 100644 --- a/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFDataBarConverterExtension.cs @@ -2,6 +2,7 @@ using ClosedXML.Extensions; using DocumentFormat.OpenXml.Office.Excel; using DocumentFormat.OpenXml.Office2010.Excel; using System.Collections.Generic; +using System.Linq; namespace ClosedXML.Excel { @@ -10,8 +11,8 @@ namespace ClosedXML.Excel private static readonly IReadOnlyDictionary CFValueToTypeMap = new Dictionary { - { DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Max, ConditionalFormattingValueObjectTypeValues.Max }, - { DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues.Min, ConditionalFormattingValueObjectTypeValues.Min }, + { 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 }, @@ -34,8 +35,25 @@ namespace ClosedXML.Excel ShowValue = !cf.ShowBarOnly }; - var cfMin = BuildValueObject(cf, 1, ConditionalFormattingValueObjectTypeValues.AutoMin); - var cfMax = BuildValueObject(cf, 2, ConditionalFormattingValueObjectTypeValues.AutoMax); + 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() }; @@ -56,50 +74,6 @@ namespace ClosedXML.Excel return conditionalFormattingRule; } - /// - /// Builds a single x14 conditional formatting value object (cfvo) for a data bar threshold. - /// - /// The conditional format the data bar belongs to. - /// 1-based threshold index (1 = minimum, 2 = maximum). - /// - /// Type emitted for an automatic bound: autoMin for the minimum, autoMax for the maximum. - /// - /// - /// Automatic and min/max bounds are self-describing and carry no formula; only - /// value based thresholds (num/percent/percentile/formula) emit an xm:f formula. - /// - private static ConditionalFormattingValueObject BuildValueObject(IXLConditionalFormat cf, int index, ConditionalFormattingValueObjectTypeValues automaticType) - { - cf.Values.TryGetValue(index, out var value); - var formulaText = value?.Value; - - if (!cf.ContentTypes.TryGetValue(index, out var contentType)) - { - return new ConditionalFormattingValueObject { Type = automaticType }; - } - - var type = GetCFType(contentType.ToOpenXml()); - var valueObject = new ConditionalFormattingValueObject { Type = type }; - - if (type == ConditionalFormattingValueObjectTypeValues.Min || - type == ConditionalFormattingValueObjectTypeValues.Max) - { - if (formulaText == null) - { - valueObject.Type = automaticType; - } - - return valueObject; - } - - if (formulaText != null) - { - valueObject.Append(new Formula { Text = formulaText }); - } - - return valueObject; - } - private static ConditionalFormattingValueObjectTypeValues GetCFType(DocumentFormat.OpenXml.Spreadsheet.ConditionalFormatValueObjectValues value) { return CFValueToTypeMap[value];