diff --git a/ClosedXML/Excel/CalcEngine/CalcEngineHelpers.cs b/ClosedXML/Excel/CalcEngine/CalcEngineHelpers.cs index c45be92..00d8a32 100644 --- a/ClosedXML/Excel/CalcEngine/CalcEngineHelpers.cs +++ b/ClosedXML/Excel/CalcEngine/CalcEngineHelpers.cs @@ -103,5 +103,27 @@ Debug.Assert(false, "failed to evaluate criteria in SumIf"); return false; } + + internal static bool ValueIsBlank(object value) + { + return + value == null || + value is string && ((string)value).Length == 0; + } + + /// + /// Get total count of cells in the specified range without initalizing them all + /// (which might cause serious performance issues on column-wide calculations). + /// + /// Expression referring to the cell range. + /// Total number of cells in the range. + internal static long GetTotalCellsCount(XObjectExpression rangeExpression) + { + var range = ((rangeExpression)?.Value as CellRangeReference)?.Range; + if (range == null) + return 0; + return (long)(range.LastColumn().ColumnNumber() - range.FirstColumn().ColumnNumber() + 1) * + (long)(range.LastRow().RowNumber() - range.FirstRow().RowNumber() + 1); + } } } diff --git a/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs b/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs index 233a7c8..4eb642e 100644 --- a/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs +++ b/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs @@ -340,20 +340,22 @@ { rangeValues.Add(value); } - var sumRangeValues = new List(); - foreach (var cell in ((CellRangeReference)sumRange.Value).Range.Cells()) + var sumRangeValues = new List(); + foreach (var value in sumRange) { - sumRangeValues.Add(cell); + sumRangeValues.Add(value); } // compute total var ce = new CalcEngine(); var tally = new Tally(); - for (var i = 0; i < Math.Min(rangeValues.Count, sumRangeValues.Count); i++) + for (var i = 0; i < Math.Max(rangeValues.Count, sumRangeValues.Count); i++) { - if (CalcEngineHelpers.ValueSatisfiesCriteria(rangeValues[i], criteria, ce)) + var targetValue = i < rangeValues.Count ? rangeValues[i] : string.Empty; + if (CalcEngineHelpers.ValueSatisfiesCriteria(targetValue, criteria, ce)) { - tally.AddValue(sumRangeValues[i].Value); + var value = i < sumRangeValues.Count ? sumRangeValues[i] : 0d; + tally.AddValue(value); } } @@ -401,7 +403,7 @@ foreach (var criteriaPair in criteriaRanges) { if (!CalcEngineHelpers.ValueSatisfiesCriteria( - criteriaPair.Item2[i], + i < criteriaPair.Item2.Count ? criteriaPair.Item2[i] : string.Empty, criteriaPair.Item1, ce)) { @@ -1028,4 +1030,4 @@ return m.Invert().mat; } } -} \ No newline at end of file +} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs b/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs index 958221e..06f720a 100644 --- a/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs +++ b/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs @@ -1,7 +1,7 @@ using ClosedXML.Excel.CalcEngine.Exceptions; using System; -using System.Collections; using System.Collections.Generic; +using System.Linq; namespace ClosedXML.Excel.CalcEngine { @@ -121,38 +121,40 @@ if ((p[0] as XObjectExpression)?.Value as CellRangeReference == null) throw new NoValueAvailableException("COUNTBLANK should have a single argument which is a range reference"); - var cnt = 0.0; var e = p[0] as XObjectExpression; + long totalCount = CalcEngineHelpers.GetTotalCellsCount(e); + long nonBlankCount = 0; foreach (var value in e) { - if (IsBlank(value)) - cnt++; + if (!CalcEngineHelpers.ValueIsBlank(value)) + nonBlankCount++; } - return cnt; - } - - internal static bool IsBlank(object value) - { - return - value == null || - value is string && ((string)value).Length == 0; + return 0d + totalCount - nonBlankCount; } private static object CountIf(List p) { CalcEngine ce = new CalcEngine(); var cnt = 0.0; - var ienum = p[0] as IEnumerable; + long processedCount = 0; + var ienum = p[0] as XObjectExpression; if (ienum != null) { + long totalCount = CalcEngineHelpers.GetTotalCellsCount(ienum); var criteria = (string)p[1].Evaluate(); foreach (var value in ienum) { if (CalcEngineHelpers.ValueSatisfiesCriteria(value, criteria, ce)) cnt++; + processedCount++; } + + // Add count of empty cells outside the used range if they match criteria + if (CalcEngineHelpers.ValueSatisfiesCriteria(string.Empty, criteria, ce)) + cnt += (totalCount - processedCount); } + return cnt; } @@ -160,15 +162,16 @@ { // get parameters var ce = new CalcEngine(); - int count = 0; + long count = 0; int numberOfCriteria = p.Count / 2; + long totalCount = 0; // prepare criteria-parameters: var criteriaRanges = new Tuple>[numberOfCriteria]; for (int criteriaPair = 0; criteriaPair < numberOfCriteria; criteriaPair++) { - var criteriaRange = p[criteriaPair * 2] as IEnumerable; + var criteriaRange = p[criteriaPair * 2] as XObjectExpression; var criterion = p[(criteriaPair * 2) + 1].Evaluate(); var criteriaRangeValues = new List(); foreach (var value in criteriaRange) @@ -179,26 +182,26 @@ criteriaRanges[criteriaPair] = new Tuple>( criterion, criteriaRangeValues); + + if (totalCount == 0) + totalCount = CalcEngineHelpers.GetTotalCellsCount(criteriaRange); } + long processedCount = 0; for (var i = 0; i < criteriaRanges[0].Item2.Count; i++) { - bool shouldUseValue = true; - - foreach (var criteriaPair in criteriaRanges) - { - if (!CalcEngineHelpers.ValueSatisfiesCriteria( - criteriaPair.Item2[i], - criteriaPair.Item1, - ce)) - { - shouldUseValue = false; - break; // we're done with the inner loop as we can't ever get true again. - } - } - - if (shouldUseValue) + if (criteriaRanges.All(criteriaPair => CalcEngineHelpers.ValueSatisfiesCriteria( + criteriaPair.Item2[i], criteriaPair.Item1, ce))) count++; + + processedCount++; + } + + // Add count of empty cells outside the used range if they match criteria + if (criteriaRanges.All(criteriaPair => CalcEngineHelpers.ValueSatisfiesCriteria( + string.Empty, criteriaPair.Item1, ce))) + { + count += (totalCount - processedCount); } // done diff --git a/ClosedXML/Excel/CalcEngine/Functions/Tally.cs b/ClosedXML/Excel/CalcEngine/Functions/Tally.cs index 8796b84..93abf67 100644 --- a/ClosedXML/Excel/CalcEngine/Functions/Tally.cs +++ b/ClosedXML/Excel/CalcEngine/Functions/Tally.cs @@ -80,7 +80,7 @@ if (numbersOnly) return NumericValuesInternal().Length; else - return _list.Count(o => !Statistical.IsBlank(o)); + return _list.Count(o => !CalcEngineHelpers.ValueIsBlank(o)); } private IEnumerable NumericValuesEnumerable() diff --git a/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs b/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs index 108ea40..da2f83b 100644 --- a/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs +++ b/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs @@ -82,7 +82,10 @@ // ** IEnumerable public IEnumerator GetEnumerator() { - return _range.Cells().Select(GetValue).GetEnumerator(); + var maxRow = Math.Min(_range.RangeAddress.LastAddress.RowNumber, _range.Worksheet.LastCellUsed().Address.RowNumber); + var maxCol = Math.Min(_range.RangeAddress.LastAddress.ColumnNumber, _range.Worksheet.LastCellUsed().Address.ColumnNumber); + using (var trimmedRange = (XLRangeBase)_range.Worksheet.Range(_range.FirstCell().Address, new XLAddress(maxRow, maxCol, false, false))) + return trimmedRange.CellValues().GetEnumerator(); } private Boolean _evaluating; @@ -90,9 +93,9 @@ // ** implementation private object GetValue(IXLCell cell) { - if (_evaluating) + if (_evaluating || (cell as XLCell).IsEvaluating) { - throw new InvalidOperationException("Circular Reference"); + throw new InvalidOperationException($"Circular Reference occured during evaluation. Cell: {cell.Address.ToString(XLReferenceStyle.Default, true)}"); } try { diff --git a/ClosedXML/Excel/Cells/IXLCell.cs b/ClosedXML/Excel/Cells/IXLCell.cs index b46bf38..274a50f 100644 --- a/ClosedXML/Excel/Cells/IXLCell.cs +++ b/ClosedXML/Excel/Cells/IXLCell.cs @@ -319,6 +319,8 @@ Boolean IsMerged(); + IXLRange MergedRange(); + Boolean IsEmpty(); Boolean IsEmpty(Boolean includeFormats); diff --git a/ClosedXML/Excel/Cells/XLCell.cs b/ClosedXML/Excel/Cells/XLCell.cs index 094119f..8a4a708 100644 --- a/ClosedXML/Excel/Cells/XLCell.cs +++ b/ClosedXML/Excel/Cells/XLCell.cs @@ -359,6 +359,11 @@ return cValue; } + /// + /// Flag showing that the cell is in formula evaluation state. + /// + internal bool IsEvaluating { get; private set; } + public object Value { get @@ -366,6 +371,9 @@ var fA1 = FormulaA1; if (!String.IsNullOrWhiteSpace(fA1)) { + if (IsEvaluating) + throw new InvalidOperationException("Circular Reference"); + if (fA1[0] == '{') fA1 = fA1.Substring(1, fA1.Length - 2); @@ -385,19 +393,31 @@ cAddress = fA1; } - if (_worksheet.Workbook.WorksheetsInternal.Any( - w => String.Compare(w.Name, sName, true) == 0) - && XLHelper.IsValidA1Address(cAddress) - ) + object retVal; + try { - var referenceCell = _worksheet.Workbook.Worksheet(sName).Cell(cAddress); - if (referenceCell.IsEmpty(false)) - return 0; - else - return referenceCell.Value; + IsEvaluating = true; + + if (_worksheet + .Workbook + .WorksheetsInternal + .Any(w => String.Compare(w.Name, sName, true) == 0) + && XLHelper.IsValidA1Address(cAddress)) + { + var referenceCell = _worksheet.Workbook.Worksheet(sName).Cell(cAddress); + if (referenceCell.IsEmpty(false)) + return 0; + else + return referenceCell.Value; + } + + retVal = Worksheet.Evaluate(fA1); + } + finally + { + IsEvaluating = false; } - var retVal = Worksheet.Evaluate(fA1); var retValEnumerable = retVal as IEnumerable; if (retValEnumerable != null && !(retVal is String)) @@ -1217,6 +1237,11 @@ return Worksheet.Internals.MergedRanges.Contains(this); } + public IXLRange MergedRange() + { + return Worksheet.Internals.MergedRanges.FirstOrDefault(r => r.Contains(this)); + } + public Boolean IsEmpty() { return IsEmpty(false); diff --git a/ClosedXML/Excel/Columns/XLColumn.cs b/ClosedXML/Excel/Columns/XLColumn.cs index 5bb1f50..9521d1d 100644 --- a/ClosedXML/Excel/Columns/XLColumn.cs +++ b/ClosedXML/Excel/Columns/XLColumn.cs @@ -623,16 +623,19 @@ private void WorksheetRangeShiftedColumns(XLRange range, int columnsShifted) { - if (range.RangeAddress.FirstAddress.ColumnNumber <= ColumnNumber()) + if (range.RangeAddress.IsValid && + RangeAddress.IsValid && + range.RangeAddress.FirstAddress.ColumnNumber <= ColumnNumber()) SetColumnNumber(ColumnNumber() + columnsShifted); } private void SetColumnNumber(int column) { if (column <= 0) - RangeAddress.IsInvalid = false; + RangeAddress.IsValid = false; else { + RangeAddress.IsValid = true; RangeAddress.FirstAddress = new XLAddress(Worksheet, 1, column, diff --git a/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs b/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs index 8ccfbdd..cae5f8c 100644 --- a/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs +++ b/ClosedXML/Excel/ConditionalFormats/IXLConditionalFormats.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; namespace ClosedXML.Excel { - public interface IXLConditionalFormats: IEnumerable + public interface IXLConditionalFormats : IEnumerable { void Add(IXLConditionalFormat conditionalFormat); + void RemoveAll(); + void Remove(Predicate predicate); } } diff --git a/ClosedXML/Excel/ConditionalFormats/Save/XLCFColorScaleConverter.cs b/ClosedXML/Excel/ConditionalFormats/Save/XLCFColorScaleConverter.cs index e7fabb8..5fc548c 100644 --- a/ClosedXML/Excel/ConditionalFormats/Save/XLCFColorScaleConverter.cs +++ b/ClosedXML/Excel/ConditionalFormats/Save/XLCFColorScaleConverter.cs @@ -24,7 +24,22 @@ for (Int32 i = 1; i <= cf.Colors.Count; i++) { - Color color = new Color { Rgb = cf.Colors[i].Color.ToHex() }; + var xlColor = cf.Colors[i]; + var color = new Color(); + switch (xlColor.ColorType) + { + case XLColorType.Color: + color.Rgb = xlColor.Color.ToHex(); + break; + case XLColorType.Theme: + color.Theme = System.Convert.ToUInt32(xlColor.ThemeColor); + break; + + case XLColorType.Indexed: + color.Indexed = System.Convert.ToUInt32(xlColor.Indexed); + break; + } + colorScale.Append(color); } diff --git a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs index 7263e7e..0140281 100644 --- a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs +++ b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormat.cs @@ -138,6 +138,7 @@ } public Guid Id { get; internal set; } + internal Int32 OriginalPriority { get; set; } public Boolean CopyDefaultModify { get; set; } public override IEnumerable Styles diff --git a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs index 940bd22..f986db8 100644 --- a/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs +++ b/ClosedXML/Excel/ConditionalFormats/XLConditionalFormats.cs @@ -110,5 +110,15 @@ _conditionalFormats.ForEach(cf => cf.Range.Dispose()); _conditionalFormats.Clear(); } + + /// + /// Reorders the according to original priority. Done during load process + /// + public void ReorderAccordingToOriginalPriority() + { + var reorderedFormats = _conditionalFormats.OrderBy(cf => (cf as XLConditionalFormat).OriginalPriority).ToList(); + _conditionalFormats.Clear(); + _conditionalFormats.AddRange(reorderedFormats); + } } } diff --git a/ClosedXML/Excel/PivotTables/XLPivotField.cs b/ClosedXML/Excel/PivotTables/XLPivotField.cs index dbdfac9..1f8ac67 100644 --- a/ClosedXML/Excel/PivotTables/XLPivotField.cs +++ b/ClosedXML/Excel/PivotTables/XLPivotField.cs @@ -5,7 +5,7 @@ namespace ClosedXML.Excel { [DebuggerDisplay("{SourceName}")] - public class XLPivotField : IXLPivotField + internal class XLPivotField : IXLPivotField { public XLPivotField(string sourceName) { diff --git a/ClosedXML/Excel/Ranges/IXLRangeAddress.cs b/ClosedXML/Excel/Ranges/IXLRangeAddress.cs index 86ed2b1..751f1a4 100644 --- a/ClosedXML/Excel/Ranges/IXLRangeAddress.cs +++ b/ClosedXML/Excel/Ranges/IXLRangeAddress.cs @@ -13,12 +13,12 @@ IXLAddress FirstAddress { get; set; } /// - /// Gets or sets a value indicating whether this range is invalid. + /// Gets or sets a value indicating whether this range is valid. /// /// - /// true if this instance is invalid; otherwise, false. + /// true if this instance is valid; otherwise, false. /// - Boolean IsInvalid { get; set; } + Boolean IsValid { get; } /// /// Gets or sets the last address in the range. diff --git a/ClosedXML/Excel/Ranges/IXLRangeBase.cs b/ClosedXML/Excel/Ranges/IXLRangeBase.cs index 4707327..0932cbf 100644 --- a/ClosedXML/Excel/Ranges/IXLRangeBase.cs +++ b/ClosedXML/Excel/Ranges/IXLRangeBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Globalization; namespace ClosedXML.Excel diff --git a/ClosedXML/Excel/Ranges/XLRangeAddress.cs b/ClosedXML/Excel/Ranges/XLRangeAddress.cs index 177677e..d900347 100644 --- a/ClosedXML/Excel/Ranges/XLRangeAddress.cs +++ b/ClosedXML/Excel/Ranges/XLRangeAddress.cs @@ -84,7 +84,7 @@ { get { - if (IsInvalid) + if (!IsValid) throw new InvalidOperationException("Range is invalid."); return _firstAddress; @@ -96,7 +96,7 @@ { get { - if (IsInvalid) + if (!IsValid) throw new InvalidOperationException("Range is an invalid state."); return _lastAddress; @@ -123,7 +123,7 @@ set { LastAddress = value as XLAddress; } } - public bool IsInvalid { get; set; } + public bool IsValid { get; set; } = true; #endregion Public properties diff --git a/ClosedXML/Excel/Ranges/XLRangeBase.cs b/ClosedXML/Excel/Ranges/XLRangeBase.cs index 9923a91..f2a9831 100644 --- a/ClosedXML/Excel/Ranges/XLRangeBase.cs +++ b/ClosedXML/Excel/Ranges/XLRangeBase.cs @@ -1,6 +1,7 @@ using ClosedXML.Excel.Misc; using ClosedXML.Extensions; using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -276,6 +277,20 @@ return Cells(true); } + /// + /// Return the collection of cell values not initializing empty cells. + /// + public IEnumerable CellValues() + { + for (int ro = RangeAddress.FirstAddress.RowNumber; ro <= RangeAddress.LastAddress.RowNumber; ro++) + { + for (int co = RangeAddress.FirstAddress.ColumnNumber; co <= RangeAddress.LastAddress.ColumnNumber; co++) + { + yield return Worksheet.GetCellValue(ro, co); + } + } + } + public IXLRange Merge() { return Merge(true); @@ -434,7 +449,7 @@ public bool Intersects(IXLRangeBase range) { - if (range.RangeAddress.IsInvalid || RangeAddress.IsInvalid) + if (!range.RangeAddress.IsValid || !RangeAddress.IsValid) return false; var ma = range.RangeAddress; var ra = RangeAddress; @@ -1519,7 +1534,7 @@ protected void ShiftColumns(IXLRangeAddress thisRangeAddress, XLRange shiftedRange, int columnsShifted) { - if (thisRangeAddress.IsInvalid || shiftedRange.RangeAddress.IsInvalid) return; + if (!thisRangeAddress.IsValid || !shiftedRange.RangeAddress.IsValid) return; bool allRowsAreCovered = thisRangeAddress.FirstAddress.RowNumber >= shiftedRange.RangeAddress.FirstAddress.RowNumber && thisRangeAddress.LastAddress.RowNumber <= shiftedRange.RangeAddress.LastAddress.RowNumber; @@ -1549,7 +1564,7 @@ if (destroyedByShift) { - thisRangeAddress.IsInvalid = true; + (thisRangeAddress as XLRangeAddress).IsValid = false; return; } @@ -1570,7 +1585,7 @@ protected void ShiftRows(IXLRangeAddress thisRangeAddress, XLRange shiftedRange, int rowsShifted) { - if (thisRangeAddress.IsInvalid || shiftedRange.RangeAddress.IsInvalid) return; + if (!thisRangeAddress.IsValid || !shiftedRange.RangeAddress.IsValid) return; bool allColumnsAreCovered = thisRangeAddress.FirstAddress.ColumnNumber >= shiftedRange.RangeAddress.FirstAddress.ColumnNumber && thisRangeAddress.LastAddress.ColumnNumber <= shiftedRange.RangeAddress.LastAddress.ColumnNumber; @@ -1600,7 +1615,7 @@ if (destroyedByShift) { - thisRangeAddress.IsInvalid = true; + (thisRangeAddress as XLRangeAddress).IsValid = false; return; } diff --git a/ClosedXML/Excel/Ranges/XLRanges.cs b/ClosedXML/Excel/Ranges/XLRanges.cs index a41fd8b..1acc4a7 100644 --- a/ClosedXML/Excel/Ranges/XLRanges.cs +++ b/ClosedXML/Excel/Ranges/XLRanges.cs @@ -61,12 +61,12 @@ public Boolean Contains(IXLCell cell) { - return _ranges.Any(r => !r.RangeAddress.IsInvalid && r.Contains(cell)); + return _ranges.Any(r => r.RangeAddress.IsValid && r.Contains(cell)); } public Boolean Contains(IXLRange range) { - return _ranges.Any(r => !r.RangeAddress.IsInvalid && r.Contains(range)); + return _ranges.Any(r => r.RangeAddress.IsValid && r.Contains(range)); } public IEnumerable DataValidation diff --git a/ClosedXML/Excel/Rows/XLRow.cs b/ClosedXML/Excel/Rows/XLRow.cs index 12da17a..53e4008 100644 --- a/ClosedXML/Excel/Rows/XLRow.cs +++ b/ClosedXML/Excel/Rows/XLRow.cs @@ -552,16 +552,19 @@ private void WorksheetRangeShiftedRows(XLRange range, int rowsShifted) { - if (range.RangeAddress.FirstAddress.RowNumber <= RowNumber()) + if (range.RangeAddress.IsValid && + RangeAddress.IsValid && + range.RangeAddress.FirstAddress.RowNumber <= RowNumber()) SetRowNumber(RowNumber() + rowsShifted); } private void SetRowNumber(Int32 row) { if (row <= 0) - RangeAddress.IsInvalid = false; + RangeAddress.IsValid = false; else { + RangeAddress.IsValid = true; RangeAddress.FirstAddress = new XLAddress(Worksheet, row, 1, RangeAddress.FirstAddress.FixedRow, RangeAddress.FirstAddress.FixedColumn); RangeAddress.LastAddress = new XLAddress(Worksheet, diff --git a/ClosedXML/Excel/XLWorkbook_Load.cs b/ClosedXML/Excel/XLWorkbook_Load.cs index 0d88e30..5e311d9 100644 --- a/ClosedXML/Excel/XLWorkbook_Load.cs +++ b/ClosedXML/Excel/XLWorkbook_Load.cs @@ -276,6 +276,8 @@ reader.Close(); } + (ws.ConditionalFormats as XLConditionalFormats).ReorderAccordingToOriginalPriority(); + #region LoadTables foreach (var tableDefinitionPart in worksheetPart.TableDefinitionParts) @@ -2145,6 +2147,7 @@ // The conditional formatting type is compulsory. If it doesn't exist, skip the entire rule. if (fr.Type == null) continue; conditionalFormat.ConditionalFormatType = fr.Type.Value.ToClosedXml(); + conditionalFormat.OriginalPriority = fr.Priority?.Value ?? Int32.MaxValue; if (conditionalFormat.ConditionalFormatType == XLConditionalFormatType.CellIs && fr.Operator != null) conditionalFormat.Operator = fr.Operator.Value.ToClosedXml(); diff --git a/ClosedXML/Excel/XLWorkbook_Save.cs b/ClosedXML/Excel/XLWorkbook_Save.cs index ebce78e..c360c17 100644 --- a/ClosedXML/Excel/XLWorkbook_Save.cs +++ b/ClosedXML/Excel/XLWorkbook_Save.cs @@ -3360,6 +3360,11 @@ AddDifferentialFormats(workbookStylesPart, context); } + /// + /// Populates the differential formats that are currently in the file to the SaveContext + /// + /// The workbook styles part. + /// The context. private void AddDifferentialFormats(WorkbookStylesPart workbookStylesPart, SaveContext context) { if (workbookStylesPart.Stylesheet.DifferentialFormats == null) @@ -3424,7 +3429,11 @@ SaveContext context) { var differentialFormat = new DifferentialFormat(); - differentialFormat.Append(GetNewFont(new FontInfo { Font = cf.Style.Value.Font }, false)); + + var diffFont = GetNewFont(new FontInfo { Font = cf.Style.Font as XLFont }, false); + if (diffFont?.HasChildren ?? false) + differentialFormat.Append(diffFont); + if (!String.IsNullOrWhiteSpace(cf.Style.NumberFormat.Format)) { var numberFormat = new NumberingFormat @@ -3434,8 +3443,14 @@ }; differentialFormat.Append(numberFormat); } - differentialFormat.Append(GetNewFill(new FillInfo { Fill = cf.Style.Value.Fill }, differentialFillFormat: true, ignoreMod: false)); - differentialFormat.Append(GetNewBorder(new BorderInfo { Border = cf.Style.Value.Border }, false)); + + var diffFill = GetNewFill(new FillInfo { Fill = cf.Style.Value.Fill }, differentialFillFormat: true, ignoreMod: false); + if (diffFill?.HasChildren ?? false) + differentialFormat.Append(diffFill); + + var diffBorder = GetNewBorder(new BorderInfo { Border = cf.Style.Value.Border }, false); + if (diffBorder?.HasChildren ?? false) + differentialFormat.Append(diffBorder); differentialFormats.Append(differentialFormat); @@ -3447,8 +3462,8 @@ { var differentialFormat = new DifferentialFormat(); - var diffFont = GetNewFont(new FontInfo {Font = style.Font}, false); - if (diffFont.HasChildren) + var diffFont = GetNewFont(new FontInfo { Font = style.Font as XLFont }, false); + if (diffFont?.HasChildren ?? false) differentialFormat.Append(diffFont); if (!String.IsNullOrWhiteSpace(style.NumberFormat.Format) || style.NumberFormat.NumberFormatId != 0) @@ -3476,11 +3491,11 @@ } var diffFill = GetNewFill(new FillInfo {Fill = style.Fill}, differentialFillFormat: true, ignoreMod: false); - if (diffFill.HasChildren) + if (diffFill?.HasChildren ?? false) differentialFormat.Append(diffFill); var diffBorder = GetNewBorder(new BorderInfo {Border = style.Border}, false); - if (diffBorder.HasChildren) + if (diffBorder?.HasChildren ?? false) differentialFormat.Append(diffBorder); differentialFormats.Append(differentialFormat); @@ -3893,7 +3908,9 @@ break; case XLColorType.Indexed: - backgroundColor.Indexed = (UInt32)fillInfo.Fill.BackgroundColor.Indexed; + // 64 is 'transparent' and should be ignored for differential formats + if (fillInfo.Fill.BackgroundColor.Indexed != 64) + backgroundColor.Indexed = (UInt32)fillInfo.Fill.BackgroundColor.Indexed; break; case XLColorType.Theme: @@ -4971,8 +4988,9 @@ worksheetPart.Worksheet.RemoveAllChildren(); var previousElement = cm.GetPreviousElementFor(XLWSContentManager.XLWSContents.ConditionalFormatting); - var priority = 1; // priority is 1 origin in Microsoft Excel - foreach (var cfGroup in xlWorksheet.ConditionalFormats + var conditionalFormats = xlWorksheet.ConditionalFormats.ToList(); // Required for IndexOf method + + foreach (var cfGroup in conditionalFormats .GroupBy( c => c.Range.RangeAddress.ToStringRelative(false), c => c, @@ -4987,8 +5005,8 @@ }; foreach (var cf in cfGroup.CfList) { + var priority = conditionalFormats.IndexOf(cf) + 1; conditionalFormatting.Append(XLCFConverters.Convert(cf, priority, context)); - priority++; } worksheetPart.Worksheet.InsertAfter(conditionalFormatting, previousElement); previousElement = conditionalFormatting; diff --git a/ClosedXML/Excel/XLWorksheet.cs b/ClosedXML/Excel/XLWorksheet.cs index c47cc79..b0648cd 100644 --- a/ClosedXML/Excel/XLWorksheet.cs +++ b/ClosedXML/Excel/XLWorksheet.cs @@ -1213,7 +1213,7 @@ cfAddress.LastAddress.RowNumber, cfAddress.LastAddress.ColumnNumber + columnsShifted); } - if (cf.Range.RangeAddress.IsInvalid || + if (!cf.Range.RangeAddress.IsValid || cf.Range.RangeAddress.FirstAddress.ColumnNumber > cf.Range.RangeAddress.LastAddress.ColumnNumber) ConditionalFormats.Remove(f => f == cf); } @@ -1287,7 +1287,7 @@ cfAddress.LastAddress.RowNumber + rowsShifted, cfAddress.LastAddress.ColumnNumber); } - if (cf.Range.RangeAddress.IsInvalid || + if (!cf.Range.RangeAddress.IsValid || cf.Range.RangeAddress.FirstAddress.RowNumber > cf.Range.RangeAddress.LastAddress.RowNumber) ConditionalFormats.Remove(f => f == cf); } @@ -1549,5 +1549,25 @@ else this.Cell(ro, co).SetValue(value); } + + /// + /// Get a cell value not initializing it if it has not been initialized yet. + /// + /// Row number + /// Column number + /// Current value of the specified cell. Empty string for non-initialized cells. + internal object GetCellValue(int ro, int co) + { + if (Internals.CellsCollection.MaxRowUsed < ro || + Internals.CellsCollection.MaxColumnUsed < co || + !Internals.CellsCollection.Contains(ro, co)) + return string.Empty; + + var cell = Worksheet.Internals.CellsCollection.GetCell(ro, co); + if (cell.IsEvaluating) + return string.Empty; + + return cell.Value; + } } } diff --git a/ClosedXML/XLHelper.cs b/ClosedXML/XLHelper.cs index dd08d44..1c1081d 100644 --- a/ClosedXML/XLHelper.cs +++ b/ClosedXML/XLHelper.cs @@ -175,7 +175,7 @@ public static Boolean IsValidRangeAddress(IXLRangeAddress rangeAddress) { - return !rangeAddress.IsInvalid + return rangeAddress.IsValid && rangeAddress.FirstAddress.RowNumber >= 1 && rangeAddress.LastAddress.RowNumber <= MaxRowNumber && rangeAddress.FirstAddress.ColumnNumber >= 1 && rangeAddress.LastAddress.ColumnNumber <= MaxColumnNumber && rangeAddress.FirstAddress.RowNumber <= rangeAddress.LastAddress.RowNumber diff --git a/ClosedXML_Tests/ClosedXML_Tests.csproj b/ClosedXML_Tests/ClosedXML_Tests.csproj index 56ecb40..43cb231 100644 --- a/ClosedXML_Tests/ClosedXML_Tests.csproj +++ b/ClosedXML_Tests/ClosedXML_Tests.csproj @@ -93,6 +93,7 @@ + @@ -312,6 +313,10 @@ + + + + diff --git a/ClosedXML_Tests/Excel/AutoFilters/AutoFilterTests.cs b/ClosedXML_Tests/Excel/AutoFilters/AutoFilterTests.cs index 6b68014..20d17f0 100644 --- a/ClosedXML_Tests/Excel/AutoFilters/AutoFilterTests.cs +++ b/ClosedXML_Tests/Excel/AutoFilters/AutoFilterTests.cs @@ -125,7 +125,7 @@ } } } - + [Test] [TestCase("A1:A4")] [TestCase("A1:B4")] @@ -152,7 +152,7 @@ range.InsertColumnsBefore(1); //Assert - Assert.IsFalse(ws.AutoFilter.Range.RangeAddress.IsInvalid); + Assert.IsTrue(ws.AutoFilter.Range.RangeAddress.IsValid); } } } diff --git a/ClosedXML_Tests/Excel/CalcEngine/StatisticalTests.cs b/ClosedXML_Tests/Excel/CalcEngine/StatisticalTests.cs index 7a432e1..3f451f1 100644 --- a/ClosedXML_Tests/Excel/CalcEngine/StatisticalTests.cs +++ b/ClosedXML_Tests/Excel/CalcEngine/StatisticalTests.cs @@ -176,7 +176,7 @@ ws.Cell(4, 3).Value = "Yes"; ws.Cell(4, 4).Value = "Yes"; - Assert.AreEqual(expectedOutcome, (int)ws.Evaluate(formula)); + Assert.AreEqual(expectedOutcome, ws.Evaluate(formula)); } } @@ -329,6 +329,36 @@ Assert.AreEqual(2189.430863, value, tolerance); } + [Test] + [TestCase("COUNT(G:I,G:G,H:I)", 258d, Description = "COUNT overlapping columns")] + [TestCase("COUNT(6:8,6:6,7:8)", 30d, Description = "COUNT overlapping rows")] + [TestCase("COUNTBLANK(H:J)", 3145640d, Description = "COUNTBLANK columns")] + [TestCase("COUNTBLANK(7:9)", 49128d, Description = "COUNTBLANK rows")] + [TestCase("COUNT(1:1048576)", 216d, Description = "COUNT worksheet")] + [TestCase("COUNTBLANK(1:1048576)", 17179868831d, Description = "COUNTBLANK worksheet")] + [TestCase("SUM(H:J)", 20501.15d, Description = "SUM columns")] + [TestCase("SUM(4:5)", 85366.12d, Description = "SUM rows")] + [TestCase("SUMIF(G:G,50,H:H)", 24.98d, Description = "SUMIF columns")] + [TestCase("SUMIF(G23:G52,\"\",H3:H32)", 53.24d, Description = "SUMIF ranges")] + [TestCase("SUMIFS(H:H,G:G,50,I:I,\">900\")", 19.99d, Description = "SUMIFS columns")] + public void TallySkipsEmptyCells(string formulaA1, double expectedResult) + { + using (var wb = SetupWorkbook()) + { + var ws = wb.Worksheets.First(); + //Let's pre-initialize cells we need so they didn't affect the result + ws.Range("A1:J45").Style.Fill.BackgroundColor = XLColor.Amber; + ws.Cell("ZZ1000").Value = 1; + int initialCount = (ws as XLWorksheet).Internals.CellsCollection.Count; + + var actualResult = (double)ws.Evaluate(formulaA1); + int cellsCount = (ws as XLWorksheet).Internals.CellsCollection.Count; + + Assert.AreEqual(expectedResult, actualResult, tolerance); + Assert.AreEqual(initialCount, cellsCount); + } + } + private XLWorkbook SetupWorkbook() { var wb = new XLWorkbook(); diff --git a/ClosedXML_Tests/Excel/Cells/XLCellTests.cs b/ClosedXML_Tests/Excel/Cells/XLCellTests.cs index a6daf62..2173c3e 100644 --- a/ClosedXML_Tests/Excel/Cells/XLCellTests.cs +++ b/ClosedXML_Tests/Excel/Cells/XLCellTests.cs @@ -524,6 +524,7 @@ { Assert.AreEqual("B1:E3", c.CurrentRegion.RangeAddress.ToString()); } + Assert.AreEqual("B1:E4", ws.Cell("E4").CurrentRegion.RangeAddress.ToString()); //// SECOND REGION @@ -531,6 +532,7 @@ { Assert.AreEqual("F1:H4", c.CurrentRegion.RangeAddress.ToString()); } + Assert.AreEqual("F1:H5", ws.Cell("F5").CurrentRegion.RangeAddress.ToString()); //// DIAGONAL @@ -613,5 +615,37 @@ Assert.AreEqual("B2", cell.FormulaA1); } } + + [Test] + public void FormulaWithCircularReferenceFails() + { + using (var wb = new XLWorkbook()) + { + var ws = wb.AddWorksheet("Sheet1"); + var A1 = ws.Cell("A1"); + var A2 = ws.Cell("A2"); + A1.FormulaA1 = "A2 + 1"; + A2.FormulaA1 = "A1 + 1"; + + Assert.Throws(() => + { + var _ = A1.Value; + }); + Assert.Throws(() => + { + var _ = A2.Value; + }); + } + } + + public void FormulaWithCircularReferenceFails2() + { + var cell = new XLWorkbook().Worksheets.Add("Sheet1").FirstCell(); + cell.FormulaA1 = "A1"; + Assert.Throws(() => + { + var _ = cell.Value; + }); + } } } diff --git a/ClosedXML_Tests/Excel/Columns/ColumnTests.cs b/ClosedXML_Tests/Excel/Columns/ColumnTests.cs index 5410863..0d13b93 100644 --- a/ClosedXML_Tests/Excel/Columns/ColumnTests.cs +++ b/ClosedXML_Tests/Excel/Columns/ColumnTests.cs @@ -237,5 +237,14 @@ Assert.AreEqual(2, lastCoUsed); } + [Test] + public void NegativeColumnNumberIsInvalid() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1") as XLWorksheet; + + var column = new XLColumn(-1, new XLColumnParameters(ws, 0, false)); + + Assert.IsFalse(column.RangeAddress.IsValid); + } } } diff --git a/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs new file mode 100644 index 0000000..983a5d6 --- /dev/null +++ b/ClosedXML_Tests/Excel/ConditionalFormats/ConditionalFormatTests.cs @@ -0,0 +1,31 @@ +using ClosedXML.Excel; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ClosedXML_Tests.Excel.ConditionalFormats +{ + [TestFixture] + public class ConditionalFormatTests + { + [Test] + public void MaintainConditionalFormattingOrder() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"StyleReferenceFiles\ConditionalFormattingOrder\inputfile.xlsx"))) + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + wb.SaveAs(ms); + return wb; + }, @"StyleReferenceFiles\ConditionalFormattingOrder\ConditionalFormattingOrder.xlsx"); + } + } + + } +} diff --git a/ClosedXML_Tests/Excel/Ranges/InsertingRangesTests.cs b/ClosedXML_Tests/Excel/Ranges/InsertingRangesTests.cs index 87e90ed..57092e6 100644 --- a/ClosedXML_Tests/Excel/Ranges/InsertingRangesTests.cs +++ b/ClosedXML_Tests/Excel/Ranges/InsertingRangesTests.cs @@ -139,7 +139,7 @@ thisRange.WorksheetRangeShiftedColumns(shiftedRange, shiftedColumns); - Assert.IsFalse(thisRange.RangeAddress.IsInvalid); + Assert.IsTrue(thisRange.RangeAddress.IsValid); Assert.AreEqual(expectedRange, thisRange.RangeAddress.ToString()); } } @@ -157,7 +157,7 @@ thisRange.WorksheetRangeShiftedColumns(shiftedRange, shiftedColumns); - Assert.IsTrue(thisRange.RangeAddress.IsInvalid); + Assert.IsFalse(thisRange.RangeAddress.IsValid); } } @@ -192,7 +192,7 @@ thisRange.WorksheetRangeShiftedRows(shiftedRange, shiftedRows); - Assert.IsFalse(thisRange.RangeAddress.IsInvalid); + Assert.IsTrue(thisRange.RangeAddress.IsValid); Assert.AreEqual(expectedRange, thisRange.RangeAddress.ToString()); } } @@ -209,7 +209,7 @@ thisRange.WorksheetRangeShiftedRows(shiftedRange, shiftedRows); - Assert.IsTrue(thisRange.RangeAddress.IsInvalid); + Assert.IsFalse(thisRange.RangeAddress.IsValid); } } } diff --git a/ClosedXML_Tests/Excel/Rows/RowTests.cs b/ClosedXML_Tests/Excel/Rows/RowTests.cs index bb74d4f..6453623 100644 --- a/ClosedXML_Tests/Excel/Rows/RowTests.cs +++ b/ClosedXML_Tests/Excel/Rows/RowTests.cs @@ -253,5 +253,15 @@ ws.Rows(1, 2).Group(); ws.Rows(1, 2).Ungroup(true); } + + [Test] + public void NegativeRowNumberIsInvalid() + { + var ws = new XLWorkbook().AddWorksheet("Sheet1") as XLWorksheet; + + var row = new XLRow(-1, new XLRowParameters(ws, 0, false)); + + Assert.IsFalse(row.RangeAddress.IsValid); + } } } diff --git a/ClosedXML_Tests/Excel/Styles/XLFillTests.cs b/ClosedXML_Tests/Excel/Styles/XLFillTests.cs index c858731..90183cb 100644 --- a/ClosedXML_Tests/Excel/Styles/XLFillTests.cs +++ b/ClosedXML_Tests/Excel/Styles/XLFillTests.cs @@ -1,5 +1,6 @@ using ClosedXML.Excel; using NUnit.Framework; +using System.IO; namespace ClosedXML_Tests.Excel { @@ -12,30 +13,30 @@ [Test] public void BackgroundColorSetsPattern() { - var fill = new XLFill {BackgroundColor = XLColor.Blue}; + var fill = new XLFill { BackgroundColor = XLColor.Blue }; Assert.AreEqual(XLFillPatternValues.Solid, fill.PatternType); } [Test] public void BackgroundNoColorSetsPatternNone() { - var fill = new XLFill {BackgroundColor = XLColor.NoColor}; + var fill = new XLFill { BackgroundColor = XLColor.NoColor }; Assert.AreEqual(XLFillPatternValues.None, fill.PatternType); } [Test] public void BackgroundPatternEqualCheck() { - var fill1 = new XLFill {BackgroundColor = XLColor.Blue}; - var fill2 = new XLFill {BackgroundColor = XLColor.Blue}; + var fill1 = new XLFill { BackgroundColor = XLColor.Blue }; + var fill2 = new XLFill { BackgroundColor = XLColor.Blue }; Assert.IsTrue(fill1.Equals(fill2)); } [Test] public void BackgroundPatternNotEqualCheck() { - var fill1 = new XLFill {BackgroundColor = XLColor.Blue}; - var fill2 = new XLFill {BackgroundColor = XLColor.Red}; + var fill1 = new XLFill { BackgroundColor = XLColor.Blue }; + var fill2 = new XLFill { BackgroundColor = XLColor.Red }; Assert.IsFalse(fill1.Equals(fill2)); } @@ -61,5 +62,20 @@ Assert.AreEqual(style.Border.LeftBorderColor, XLColor.Blue); Assert.AreEqual(style.Border.RightBorderColor, XLColor.Blue); } + + [Test] + public void LoadAndSaveTransparentBackgroundFill() + { + using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"StyleReferenceFiles\TransparentBackgroundFill\inputfile.xlsx"))) + using (var ms = new MemoryStream()) + { + TestHelper.CreateAndCompare(() => + { + var wb = new XLWorkbook(stream); + wb.SaveAs(ms); + return wb; + }, @"StyleReferenceFiles\TransparentBackgroundFill\TransparentBackgroundFill.xlsx"); + } + } } } diff --git a/ClosedXML_Tests/Excel/Tables/TablesTests.cs b/ClosedXML_Tests/Excel/Tables/TablesTests.cs index f8fcddc..ca50c3f 100644 --- a/ClosedXML_Tests/Excel/Tables/TablesTests.cs +++ b/ClosedXML_Tests/Excel/Tables/TablesTests.cs @@ -455,22 +455,19 @@ { var ws = wb.AddWorksheet("Sheet1"); ws.FirstCell().InsertTable(l); - //wb.SaveAs(ms); - - wb.SaveAs(@"c:\temp\deletetable1.xlsx"); + wb.SaveAs(ms); } ms.Seek(0, SeekOrigin.Begin); - using (var wb = new XLWorkbook(@"c:\temp\deletetable1.xlsx")) + using (var wb = new XLWorkbook(ms)) { var ws = wb.Worksheets.First(); var table = ws.Tables.First(); ws.Tables.Remove(table.Name); Assert.AreEqual(0, ws.Tables.Count()); - //wb.Save(); - wb.SaveAs(@"c:\temp\deletetable2.xlsx"); + wb.Save(); } } } diff --git a/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs b/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs index 3a41da8..f87f008 100644 --- a/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs +++ b/ClosedXML_Tests/Excel/Worksheets/XLWorksheetTests.cs @@ -110,6 +110,11 @@ Assert.AreEqual(2, ws.MergedRanges.Count); Assert.AreEqual("A1:B2", ws.MergedRanges.First().RangeAddress.ToStringRelative()); Assert.AreEqual("D2:E2", ws.MergedRanges.Last().RangeAddress.ToStringRelative()); + + Assert.AreEqual("A1:B2", ws.Cell("A2").MergedRange().RangeAddress.ToStringRelative()); + Assert.AreEqual("D2:E2", ws.Cell("D2").MergedRange().RangeAddress.ToStringRelative()); + + Assert.AreEqual(null, ws.Cell("Z10").MergedRange()); } [Test] diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFContains.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFContains.xlsx index a0c3791..8e4adf1 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFContains.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFContains.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDatesOccurring.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDatesOccurring.xlsx index 6ec8da4..94f5caa 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDatesOccurring.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFDatesOccurring.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEndsWith.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEndsWith.xlsx index bfb06cd..80d08b7 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEndsWith.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEndsWith.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsNumber.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsNumber.xlsx index bbfa6d4..13f828d 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsNumber.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsNumber.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsString.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsString.xlsx index 4b6a481..8a25637 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsString.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFEqualsString.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsBlank.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsBlank.xlsx index 84f5931..e6fa5ec 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsBlank.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsBlank.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsError.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsError.xlsx index 0677026..ad1c538 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsError.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFIsError.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFMultipleConditions.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFMultipleConditions.xlsx index d396c20..565af0c 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFMultipleConditions.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFMultipleConditions.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotBlank.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotBlank.xlsx index f2a8f5b..8861be4 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotBlank.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotBlank.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotContains.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotContains.xlsx index 22026b6..8656701 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotContains.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotContains.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsNumber.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsNumber.xlsx index 4eabc7c..6e23b6a 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsNumber.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsNumber.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsString.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsString.xlsx index 72e5631..d39adb0 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsString.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotEqualsString.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotError.xlsx b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotError.xlsx index c808164..1ca0131 100644 --- a/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotError.xlsx +++ b/ClosedXML_Tests/Resource/Examples/ConditionalFormatting/CFNotError.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/StyleReferenceFiles/ConditionalFormattingOrder/ConditionalFormattingOrder.xlsx b/ClosedXML_Tests/Resource/StyleReferenceFiles/ConditionalFormattingOrder/ConditionalFormattingOrder.xlsx new file mode 100644 index 0000000..5f27c54 --- /dev/null +++ b/ClosedXML_Tests/Resource/StyleReferenceFiles/ConditionalFormattingOrder/ConditionalFormattingOrder.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/StyleReferenceFiles/ConditionalFormattingOrder/inputfile.xlsx b/ClosedXML_Tests/Resource/StyleReferenceFiles/ConditionalFormattingOrder/inputfile.xlsx new file mode 100644 index 0000000..28a7fcd --- /dev/null +++ b/ClosedXML_Tests/Resource/StyleReferenceFiles/ConditionalFormattingOrder/inputfile.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/StyleReferenceFiles/TransparentBackgroundFill/TransparentBackgroundFill.xlsx b/ClosedXML_Tests/Resource/StyleReferenceFiles/TransparentBackgroundFill/TransparentBackgroundFill.xlsx new file mode 100644 index 0000000..3a12637 --- /dev/null +++ b/ClosedXML_Tests/Resource/StyleReferenceFiles/TransparentBackgroundFill/TransparentBackgroundFill.xlsx Binary files differ diff --git a/ClosedXML_Tests/Resource/StyleReferenceFiles/TransparentBackgroundFill/inputfile.xlsx b/ClosedXML_Tests/Resource/StyleReferenceFiles/TransparentBackgroundFill/inputfile.xlsx new file mode 100644 index 0000000..6d6f1a8 --- /dev/null +++ b/ClosedXML_Tests/Resource/StyleReferenceFiles/TransparentBackgroundFill/inputfile.xlsx Binary files differ diff --git a/ClosedXML_Tests/TestHelper.cs b/ClosedXML_Tests/TestHelper.cs index 646d9d0..5c1acf8 100644 --- a/ClosedXML_Tests/TestHelper.cs +++ b/ClosedXML_Tests/TestHelper.cs @@ -22,16 +22,17 @@ { get { - return Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + return Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "Generated"); } } public const string ActualTestResultPostFix = ""; - public static readonly string TestsExampleOutputDirectory = Path.Combine(TestsOutputDirectory, "Examples"); + public static readonly string ExampleTestsOutputDirectory = Path.Combine(TestsOutputDirectory, "Examples"); + public static readonly string OtherTestsOutputDirectory = Path.Combine(TestsOutputDirectory, "Other"); private const bool CompareWithResources = true; - private static readonly ResourceFileExtractor _extractor = new ResourceFileExtractor(null, ".Resource.Examples."); + private static readonly ResourceFileExtractor _extractor = new ResourceFileExtractor(null, ".Resource."); public static void SaveWorkbook(XLWorkbook workbook, params string[] fileNameParts) { @@ -59,7 +60,7 @@ var example = new T(); string[] pathParts = filePartName.Split(new char[] { '\\' }); - string filePath1 = Path.Combine(new List() { TestsExampleOutputDirectory }.Concat(pathParts).ToArray()); + string filePath1 = Path.Combine(new List() { ExampleTestsOutputDirectory }.Concat(pathParts).ToArray()); var extension = Path.GetExtension(filePath1); var directory = Path.GetDirectoryName(filePath1); @@ -76,9 +77,46 @@ wb.SaveAs(filePath2, true, evaluateFormulae); if (CompareWithResources) - { - string resourcePath = filePartName.Replace('\\', '.').TrimStart('.'); + string resourcePath = "Examples." + filePartName.Replace('\\', '.').TrimStart('.'); + using (var streamExpected = _extractor.ReadFileFromResToStream(resourcePath)) + using (var streamActual = File.OpenRead(filePath2)) + { + string message; + var success = ExcelDocsComparer.Compare(streamActual, streamExpected, TestHelper.IsRunningOnUnix, out message); + var formattedMessage = + String.Format( + "Actual file '{0}' is different than the expected file '{1}'. The difference is: '{2}'", + filePath2, resourcePath, message); + + Assert.IsTrue(success, formattedMessage); + } + } + } + + public static void CreateAndCompare(Func workbookGenerator, string referenceResource, bool evaluateFormulae = false) + { + Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US"); + + string[] pathParts = referenceResource.Split(new char[] { '\\' }); + string filePath1 = Path.Combine(new List() { OtherTestsOutputDirectory }.Concat(pathParts).ToArray()); + + var extension = Path.GetExtension(filePath1); + var directory = Path.GetDirectoryName(filePath1); + + var fileName = Path.GetFileNameWithoutExtension(filePath1); + fileName += ActualTestResultPostFix; + fileName = Path.ChangeExtension(fileName, extension); + + filePath1 = Path.Combine(directory, "z" + fileName); + var filePath2 = Path.Combine(directory, fileName); + + using (var wb = workbookGenerator.Invoke()) + wb.SaveAs(filePath2, true, evaluateFormulae); + + if (CompareWithResources) + { + string resourcePath = referenceResource.Replace('\\', '.').TrimStart('.'); using (var streamExpected = _extractor.ReadFileFromResToStream(resourcePath)) using (var streamActual = File.OpenRead(filePath2)) { diff --git a/ClosedXML_Tests/Utils/PackageHelper.cs b/ClosedXML_Tests/Utils/PackageHelper.cs index 3def760..60bc482 100644 --- a/ClosedXML_Tests/Utils/PackageHelper.cs +++ b/ClosedXML_Tests/Utils/PackageHelper.cs @@ -89,7 +89,7 @@ throw new ArgumentNullException("dest"); } - #endregion + #endregion Check if (dest.PartExists(uri)) { @@ -130,7 +130,7 @@ throw new ArgumentNullException("serializeAction"); } - #endregion + #endregion Check if (package.PartExists(descriptor.Uri)) { @@ -160,7 +160,7 @@ throw new ArgumentNullException("serializeAction"); } - #endregion + #endregion Check if (package.PartExists(descriptor.Uri)) { @@ -190,7 +190,7 @@ throw new ArgumentNullException("deserializeFunc"); } - #endregion + #endregion Check if (!package.PartExists(uri)) { @@ -220,7 +220,7 @@ throw new ArgumentNullException("deserializeAction"); } - #endregion + #endregion Check if (!package.PartExists(uri)) { @@ -250,7 +250,7 @@ throw new ArgumentNullException("deserializeAction"); } - #endregion + #endregion Check if (!package.PartExists(uri)) { @@ -301,7 +301,7 @@ throw new ArgumentNullException("right"); } - #endregion + #endregion Check excludeMethod = excludeMethod ?? (uri => false); PackagePartCollection leftParts = left.GetParts(); @@ -353,7 +353,10 @@ leftPart.ContentType == @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" && rightPart.ContentType == @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"; - if (!StreamHelper.Compare(oneStream, otherStream, stripColumnWidthsFromSheet)) + var tuple1 = new Tuple(pair.Uri, oneStream); + var tuple2 = new Tuple(pair.Uri, otherStream); + + if (!StreamHelper.Compare(tuple1, tuple2, stripColumnWidthsFromSheet)) { pair.Status = CompareStatus.NonEqual; if (compareToFirstDifference) @@ -387,11 +390,16 @@ { #region Private fields - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly CompressionOption _compressOption; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly string _contentType; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Uri _uri; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly CompressionOption _compressOption; - #endregion + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly string _contentType; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Uri _uri; + + #endregion Private fields #region Constructor @@ -414,33 +422,36 @@ throw new ArgumentNullException("contentType"); } - #endregion + #endregion Check _uri = uri; _contentType = contentType; _compressOption = compressOption; } - #endregion + #endregion Constructor #region Public properties public Uri Uri { - [DebuggerStepThrough] get { return _uri; } + [DebuggerStepThrough] + get { return _uri; } } public string ContentType { - [DebuggerStepThrough] get { return _contentType; } + [DebuggerStepThrough] + get { return _contentType; } } public CompressionOption CompressOption { - [DebuggerStepThrough] get { return _compressOption; } + [DebuggerStepThrough] + get { return _compressOption; } } - #endregion + #endregion Public properties #region Public methods @@ -449,10 +460,10 @@ return string.Format("Uri:{0} ContentType: {1}, Compression: {2}", _uri, _contentType, _compressOption); } - #endregion + #endregion Public methods } - #endregion + #endregion Nested type: PackagePartDescriptor #region Nested type: CompareStatus @@ -464,7 +475,7 @@ NonEqual } - #endregion + #endregion Nested type: CompareStatus #region Nested type: PartPair @@ -472,10 +483,13 @@ { #region Private fields - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly Uri _uri; - [DebuggerBrowsable(DebuggerBrowsableState.Never)] private CompareStatus _status; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private readonly Uri _uri; - #endregion + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private CompareStatus _status; + + #endregion Private fields #region Constructor @@ -485,26 +499,29 @@ _status = status; } - #endregion + #endregion Constructor #region Public properties public Uri Uri { - [DebuggerStepThrough] get { return _uri; } + [DebuggerStepThrough] + get { return _uri; } } public CompareStatus Status { - [DebuggerStepThrough] get { return _status; } - [DebuggerStepThrough] set { _status = value; } + [DebuggerStepThrough] + get { return _status; } + [DebuggerStepThrough] + set { _status = value; } } - #endregion + #endregion Public properties } - #endregion + #endregion Nested type: PartPair //-- } -} \ No newline at end of file +} diff --git a/ClosedXML_Tests/Utils/StreamHelper.cs b/ClosedXML_Tests/Utils/StreamHelper.cs index 8ecb4f9..bf89911 100644 --- a/ClosedXML_Tests/Utils/StreamHelper.cs +++ b/ClosedXML_Tests/Utils/StreamHelper.cs @@ -114,36 +114,39 @@ /// /// /// /// - public static bool Compare(Stream one, Stream other, bool stripColumnWidths) + public static bool Compare(Tuple tuple1, Tuple tuple2, bool stripColumnWidths) { #region Check - if (one == null) + if (tuple1 == null || tuple1.Item1 == null || tuple1.Item2 == null) { throw new ArgumentNullException("one"); } - if (other == null) + if (tuple2 == null || tuple2.Item1 == null || tuple2.Item2 == null) { throw new ArgumentNullException("other"); } - if (one.Position != 0) + if (tuple1.Item2.Position != 0) { throw new ArgumentException("Must be in position 0", "one"); } - if (other.Position != 0) + if (tuple1.Item2.Position != 0) { throw new ArgumentException("Must be in position 0", "other"); } #endregion Check - var stringOne = new StreamReader(one).ReadToEnd().RemoveIgnoredParts(stripColumnWidths, ignoreGuids: true); - var stringOther = new StreamReader(other).ReadToEnd().RemoveIgnoredParts(stripColumnWidths, ignoreGuids: true); + var stringOne = new StreamReader(tuple1.Item2).ReadToEnd().RemoveIgnoredParts(tuple1.Item1, stripColumnWidths, ignoreGuids: true); + var stringOther = new StreamReader(tuple2.Item2).ReadToEnd().RemoveIgnoredParts(tuple2.Item1, stripColumnWidths, ignoreGuids: true); return stringOne == stringOther; } - private static string RemoveIgnoredParts(this string s, Boolean ignoreColumnWidths, Boolean ignoreGuids) + private static string RemoveIgnoredParts(this string s, Uri uri, Boolean ignoreColumnWidths, Boolean ignoreGuids) { + foreach (var pair in uriSpecificIgnores.Where(p => p.Key.Equals(uri.OriginalString))) + s = pair.Value.Replace(s, ""); + if (ignoreColumnWidths) s = RemoveColumnWidths(s); @@ -153,6 +156,12 @@ return s; } + private static IEnumerable> uriSpecificIgnores = new List>() + { + // Remove dcterms elements + new KeyValuePair("/docProps/core.xml", new Regex(@"", RegexOptions.Compiled)) + }; + private static Regex columnRegex = new Regex("", RegexOptions.Compiled); private static Regex widthRegex = new Regex("width=\"\\d+(\\.\\d+)?\"\\s+", RegexOptions.Compiled);