diff --git a/ClosedXML/Excel/Cells/XLCell.cs b/ClosedXML/Excel/Cells/XLCell.cs index 0c335d5..b47bb3b 100644 --- a/ClosedXML/Excel/Cells/XLCell.cs +++ b/ClosedXML/Excel/Cells/XLCell.cs @@ -1925,7 +1925,7 @@ return _formatCodes; } - private string GetFormulaR1C1(string value) + internal string GetFormulaR1C1(string value) { return GetFormula(value, FormulaConversionType.A1ToR1C1, 0, 0); } diff --git a/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs b/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs index 8ccfbdd..541b12f 100644 --- a/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs +++ b/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs @@ -10,5 +10,6 @@ void Add(IXLConditionalFormat conditionalFormat); void RemoveAll(); void Remove(Predicate predicate); + void Compress(); } } diff --git a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs index 03aa8ca..5e665f8 100644 --- a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs +++ b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs @@ -1,11 +1,80 @@ using System; using System.Collections.Generic; +using System.Linq; using ClosedXML.Utils; namespace ClosedXML.Excel { internal class XLConditionalFormat : IXLConditionalFormat, IXLStylized { + private sealed class FullEqualityComparer : IEqualityComparer + { + private readonly bool _compareRange; + private readonly DictionaryComparer _colorsComparer = new DictionaryComparer(); + private readonly EnumerableComparer _listComparer = new EnumerableComparer(); + private readonly DictionaryComparer _contentsTypeComparer = new DictionaryComparer(); + private readonly DictionaryComparer _iconSetTypeComparer = new DictionaryComparer(); + + public FullEqualityComparer(bool compareRange) + { + _compareRange = compareRange; + } + + public bool Equals(IXLConditionalFormat x, IXLConditionalFormat y) + { + var xx = (XLConditionalFormat) x; + var yy = (XLConditionalFormat) y; + if (ReferenceEquals(xx, yy)) return true; + if (ReferenceEquals(xx, null)) return false; + if (ReferenceEquals(yy, null)) return false; + if (xx.GetType() != yy.GetType()) return false; + + var xxValues = xx.Values.Values.Where(v => !v.IsFormula).Select(v=>v.Value); + var yyValues = yy.Values.Values.Where(v => !v.IsFormula).Select(v => v.Value); + var xxFormulas = xx.Values.Values.Where(v => v.IsFormula).Select(f => ((XLCell)x.Range.FirstCell()).GetFormulaR1C1(f.Value)); + var yyFormulas = yy.Values.Values.Where(v => v.IsFormula).Select(f => ((XLCell)y.Range.FirstCell()).GetFormulaR1C1(f.Value)); + + var xStyle = xx._style ?? xx.Range.Worksheet.Workbook.GetStyleById(xx._styleCacheId); + var yStyle = yy._style ?? yy.Range.Worksheet.Workbook.GetStyleById(yy._styleCacheId); + + return Equals(xStyle, yStyle) + && xx.CopyDefaultModify == yy.CopyDefaultModify + && xx.UpdatingStyle == yy.UpdatingStyle + && xx.ConditionalFormatType == yy.ConditionalFormatType + && xx.TimePeriod == yy.TimePeriod + && xx.IconSetStyle == yy.IconSetStyle + && xx.Operator == yy.Operator + && xx.Bottom == yy.Bottom + && xx.Percent == yy.Percent + && xx.ReverseIconOrder == yy.ReverseIconOrder + && xx.ShowIconOnly == yy.ShowIconOnly + && xx.ShowBarOnly == yy.ShowBarOnly + && _listComparer.Equals(xxValues, yyValues) + && _listComparer.Equals(xxFormulas, yyFormulas) + && _colorsComparer.Equals(xx.Colors, yy.Colors) + && _contentsTypeComparer.Equals(xx.ContentTypes, yy.ContentTypes) + && _iconSetTypeComparer.Equals(xx.IconSetOperators, yy.IconSetOperators) + && (!_compareRange || Equals(xx.Range.RangeAddress, yy.Range.RangeAddress)) ; + } + + public int GetHashCode(IXLConditionalFormat obj) + { + throw new NotImplementedException(); + } + } + + private static readonly IEqualityComparer FullComparerInstance = new FullEqualityComparer(true); + public static IEqualityComparer FullComparer + { + get { return FullComparerInstance; } + } + + private static readonly IEqualityComparer NoRangeComparerInstance = new FullEqualityComparer(false); + public static IEqualityComparer NoRangeComparer + { + get { return NoRangeComparerInstance; } + } + public XLConditionalFormat(XLRange range, Boolean copyDefaultModify = false) { Id = Guid.NewGuid(); @@ -376,5 +445,59 @@ return new XLCFIconSet(this); } } + + internal class DictionaryComparer : + IEqualityComparer> + { + private readonly IEqualityComparer _valueComparer; + public DictionaryComparer(IEqualityComparer valueComparer = null) + { + this._valueComparer = valueComparer ?? EqualityComparer.Default; + } + public bool Equals(Dictionary x, Dictionary y) + { + if (x.Count != y.Count) + return false; + if (x.Keys.Except(y.Keys).Any()) + return false; + if (y.Keys.Except(x.Keys).Any()) + return false; + foreach (var pair in x) + if (!_valueComparer.Equals(pair.Value, y[pair.Key])) + return false; + return true; + } + + public int GetHashCode(Dictionary obj) + { + throw new NotImplementedException(); + } + } + + internal class EnumerableComparer : IEqualityComparer> + { + private readonly IEqualityComparer _valueComparer; + public EnumerableComparer(IEqualityComparer valueComparer = null) + { + this._valueComparer = valueComparer ?? EqualityComparer.Default; + } + + public bool Equals(IEnumerable x, IEnumerable y) + { + return SetEquals(x, y, _valueComparer); + } + + public int GetHashCode(IEnumerable obj) + { + throw new NotImplementedException(); + } + + public static bool SetEquals(IEnumerable first, IEnumerable second, + IEqualityComparer comparer) + { + return new HashSet(second, comparer ?? EqualityComparer.Default) + .SetEquals(first); + } + } } diff --git a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs index c968484..e9b04bf 100644 --- a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs +++ b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs @@ -13,6 +13,22 @@ _conditionalFormats.Add(conditionalFormat); } + private bool RangeAbove(IXLRangeAddress newAddr, IXLRangeAddress addr) + { + return newAddr.FirstAddress.ColumnNumber == addr.FirstAddress.ColumnNumber + && newAddr.LastAddress.ColumnNumber == addr.LastAddress.ColumnNumber + && newAddr.FirstAddress.RowNumber < addr.FirstAddress.RowNumber + && (newAddr.LastAddress.RowNumber+1).Between(addr.FirstAddress.RowNumber, addr.LastAddress.RowNumber); + } + + private bool RangeBefore(IXLRangeAddress newAddr, IXLRangeAddress addr) + { + return newAddr.FirstAddress.RowNumber == addr.FirstAddress.RowNumber + && newAddr.LastAddress.RowNumber == addr.LastAddress.RowNumber + && newAddr.FirstAddress.ColumnNumber < addr.FirstAddress.ColumnNumber + && (newAddr.LastAddress.ColumnNumber+1).Between(addr.FirstAddress.ColumnNumber, addr.LastAddress.ColumnNumber); + } + public IEnumerator GetEnumerator() { return _conditionalFormats.GetEnumerator(); @@ -29,6 +45,52 @@ _conditionalFormats.RemoveAll(predicate); } + public void Compress() + { + var formats = _conditionalFormats + .OrderByDescending(x=>x.Range.RangeAddress.FirstAddress.RowNumber) + .ThenByDescending(x=>x.Range.RangeAddress.FirstAddress.ColumnNumber) + .ToList(); + foreach (var item in formats) + { + var addr = item.Range.RangeAddress; + var sameFormats = _conditionalFormats.Where( + f => f != item + && f.Range.Worksheet.Position == item.Range.Worksheet.Position + && XLConditionalFormat.NoRangeComparer.Equals(f, item)) + .ToArray(); + + var format = sameFormats.FirstOrDefault(f => f.Range.Contains(item.Range)); + if (format != null) + { + _conditionalFormats.Remove(item); + continue; + } + + format = sameFormats.FirstOrDefault(f => RangeAbove(addr, f.Range.RangeAddress) || RangeBefore(addr, f.Range.RangeAddress)); + if (format != null) + { + Merge(format, item); + _conditionalFormats.Remove(item); + // compress with bottom range + var newaddr = format.Range.RangeAddress; + var bottom = sameFormats.FirstOrDefault(f => RangeAbove(newaddr, f.Range.RangeAddress)); + if (bottom != null) + { + Merge(bottom, format); + _conditionalFormats.Remove(format); + } + } + } + } + + private static void Merge(IXLConditionalFormat format, IXLConditionalFormat item) + { + foreach (var v in format.Values.ToList()) + format.Values[v.Key] = item.Values[v.Key]; + format.Range.RangeAddress.FirstAddress = item.Range.RangeAddress.FirstAddress; + } + public void RemoveAll() { _conditionalFormats.ForEach(cf => cf.Range.Dispose()); diff --git a/ClosedXML/Excel/XLWorkbook.cs b/ClosedXML/Excel/XLWorkbook.cs index 1177678..0f1c2ea 100644 --- a/ClosedXML/Excel/XLWorkbook.cs +++ b/ClosedXML/Excel/XLWorkbook.cs @@ -458,6 +458,11 @@ if (_loadSource == XLLoadSource.New) throw new InvalidOperationException("This is a new file. Please use one of the 'SaveAs' methods."); + foreach (var worksheet in Worksheets) + { + worksheet.ConditionalFormats.Compress(); + } + if (_loadSource == XLLoadSource.Stream) { CreatePackage(_originalStream, false, _spreadsheetDocumentType, options); @@ -494,6 +499,12 @@ public void SaveAs(String file, SaveOptions options) { checkForWorksheetsPresent(); + + foreach (var worksheet in Worksheets) + { + worksheet.ConditionalFormats.Compress(); + } + PathHelper.CreateDirectory(Path.GetDirectoryName(file)); if (_loadSource == XLLoadSource.New) { @@ -581,6 +592,12 @@ public void SaveAs(Stream stream, SaveOptions options) { checkForWorksheetsPresent(); + + foreach (var worksheet in Worksheets) + { + worksheet.ConditionalFormats.Compress(); + } + if (_loadSource == XLLoadSource.New) { // dm 20130422, this method or better the method SpreadsheetDocument.Create which is called diff --git a/ClosedXML/Excel/XLWorksheet.cs b/ClosedXML/Excel/XLWorksheet.cs index 6b5348a..9b48ed2 100644 --- a/ClosedXML/Excel/XLWorksheet.cs +++ b/ClosedXML/Excel/XLWorksheet.cs @@ -1350,6 +1350,7 @@ conditionalFormat.Range.Dispose(); } ResumeEvents(); + newConditionalFormats.Compress(); ConditionalFormats = newConditionalFormats; } diff --git a/ClosedXML/Extensions.cs b/ClosedXML/Extensions.cs index 6cde6da..6d9cbe7 100644 --- a/ClosedXML/Extensions.cs +++ b/ClosedXML/Extensions.cs @@ -176,6 +176,11 @@ { return value.ToString(CultureInfo.InvariantCulture.NumberFormat); } + + public static bool Between(this int val, int from, int to) + { + return val >= from && val <= to; + } } public static class DecimalExtensions diff --git a/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatesCompressTests.cs b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatesCompressTests.cs new file mode 100644 index 0000000..fc0748a --- /dev/null +++ b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatesCompressTests.cs @@ -0,0 +1,136 @@ +using System.Linq; +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.ConditionalFormats +{ + [TestFixture] + public class ConditionalFormatesCompressTests + { + [Test] + public void ConsecutivelyRowsCompressTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B2:C2").AddConditionalFormat()); + SetFormat1(ws.Range("B4:C4").AddConditionalFormat()); + SetFormat1(ws.Range("B3:C3").AddConditionalFormat()); + + ws.ConditionalFormats.Compress(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B2:C4", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F2", format.Values.Values.First().Value); + } + + [Test] + public void ConsecutivelyColumnsCompressTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("D2:D3").AddConditionalFormat()); + SetFormat1(ws.Range("B2:B3").AddConditionalFormat()); + SetFormat1(ws.Range("C2:C3").AddConditionalFormat()); + + ws.ConditionalFormats.Compress(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B2:D3", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F2", format.Values.Values.First().Value); + } + + [Test] + public void Contains1CompressTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B11:D12").AddConditionalFormat()); + SetFormat1(ws.Range("C12:D12").AddConditionalFormat()); + + ws.ConditionalFormats.Compress(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B11:D12", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F11", format.Values.Values.First().Value); + } + + [Test] + public void Contains2CompressTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B14:C14").AddConditionalFormat()); + SetFormat1(ws.Range("B14:B14").AddConditionalFormat()); + + ws.ConditionalFormats.Compress(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B14:C14", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F14", format.Values.Values.First().Value); + } + + [Test] + public void SuperimposedCompressTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B16:D18").AddConditionalFormat()); + SetFormat1(ws.Range("B18:D19").AddConditionalFormat()); + + ws.ConditionalFormats.Compress(); + + Assert.AreEqual(1, ws.ConditionalFormats.Count()); + var format = ws.ConditionalFormats.First(); + Assert.AreEqual("B16:D19", format.Range.RangeAddress.ToStringRelative()); + Assert.AreEqual("F16", format.Values.Values.First().Value); + } + + [Test] + public void DifferentRangesNoCompressTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B7:C7").AddConditionalFormat()); + SetFormat1(ws.Range("B8:B8").AddConditionalFormat()); + SetFormat1(ws.Range("B9:C9").AddConditionalFormat()); + + ws.ConditionalFormats.Compress(); + + Assert.AreEqual(3, ws.ConditionalFormats.Count()); + } + + [Test] + public void DifferentFormatNoCompressTest() + { + var wb = new XLWorkbook(); + IXLWorksheet ws = wb.Worksheets.Add("Sheet"); + + SetFormat1(ws.Range("B11:D12").AddConditionalFormat()); + SetFormat2(ws.Range("C12:D12").AddConditionalFormat()); + + ws.ConditionalFormats.Compress(); + + Assert.AreEqual(2, ws.ConditionalFormats.Count()); + } + + private static void SetFormat1(IXLConditionalFormat format) + { + format.WhenEquals("="+format.Range.FirstCell().CellRight(4).Address.ToStringRelative()).Fill.SetBackgroundColor(XLColor.Blue); + } + + private static void SetFormat2(IXLConditionalFormat format) + { + format.WhenEquals(5).Fill.SetBackgroundColor(XLColor.AliceBlue); + } + } +} \ No newline at end of file