diff --git a/.gitignore b/.gitignore index fd71b28..7064b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -290,3 +290,4 @@ __pycache__/ *.odx.cs *.xsd.cs nuget.txt +/Benchmark/BenchmarkDotNet.Artifacts/* diff --git a/ClosedXML.Report.Benchmarks/Benchmarks/ReportBenchmarks.cs b/ClosedXML.Report.Benchmarks/Benchmarks/ReportBenchmarks.cs new file mode 100644 index 0000000..5abc2c6 --- /dev/null +++ b/ClosedXML.Report.Benchmarks/Benchmarks/ReportBenchmarks.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using BenchmarkDotNet.Attributes; +using ClosedXML.Report.Benchmarks.Models; + +namespace ClosedXML.Report.Benchmarks.Benchmarks; + +[MemoryDiagnoser] +public class ReportBenchmarks +{ + private Customer _customer = null!; + private byte[] _templateData = []; + const string ResourceName = "ClosedXML.Report.Benchmarks.Resources.Benchmark.xlsx"; + + [GlobalSetup] + public void Setup() + { + // Load embedded resource outside of the benchmark + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ResourceName); + if (stream == null) + throw new Exception($"Resource {ResourceName} not found"); + + // Create a memory stream to hold the resource data + var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + _templateData = memoryStream.ToArray(); + memoryStream.Position = 0; + + // Prepare data + var dataBuilder = new DataBuilder(); + _customer = dataBuilder.Create(); + } + + [Benchmark] + public void ReportGeneration() + { + using var memoryStream = new MemoryStream(_templateData); + using var xlTemplate = new XLTemplate(memoryStream); + + // Add variables and generate report + xlTemplate.AddVariable(_customer); + xlTemplate.Generate(); + + //Not testing the save, as it has a high overhead. + //xlTemplate.SaveAs(Path.Combine("Output", "BenchmarkOutput.xlsx")); + } +} \ No newline at end of file diff --git a/ClosedXML.Report.Benchmarks/ClosedXML.Report.Benchmarks.csproj b/ClosedXML.Report.Benchmarks/ClosedXML.Report.Benchmarks.csproj new file mode 100644 index 0000000..51cd886 --- /dev/null +++ b/ClosedXML.Report.Benchmarks/ClosedXML.Report.Benchmarks.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + default + + + + + + + + + + + + + + + + + diff --git a/ClosedXML.Report.Benchmarks/Models/Customer.cs b/ClosedXML.Report.Benchmarks/Models/Customer.cs new file mode 100644 index 0000000..ab5a144 --- /dev/null +++ b/ClosedXML.Report.Benchmarks/Models/Customer.cs @@ -0,0 +1,27 @@ +namespace ClosedXML.Report.Benchmarks.Models; + +public class Customer +{ + public required string Company { get; set; } + + public required string Addr1 { get; set; } + + public required string Addr2 { get; set; } + + + public required string City { get; set; } + + public required string State { get; set; } + + public required string Country { get; set; } + + public required string Phone { get; set; } + + public required string Email { get; set; } + + public required string Zip { get; set; } + + public string Fax { get; set; } = string.Empty; + + public List Orders { get; init; } = []; +} \ No newline at end of file diff --git a/ClosedXML.Report.Benchmarks/Models/Order.cs b/ClosedXML.Report.Benchmarks/Models/Order.cs new file mode 100644 index 0000000..5221c57 --- /dev/null +++ b/ClosedXML.Report.Benchmarks/Models/Order.cs @@ -0,0 +1,22 @@ +namespace ClosedXML.Report.Benchmarks.Models; + +public class Order +{ + public required string OrderNo { get; set; } + + public DateTime SaleDate { get; set; } + + public DateTime ShipDate { get; set; } + + public string ShipToAddr1 { get; set; } = string.Empty; + + public string ShipToAddr2 { get; set; } = string.Empty; + + public string PaymentMethod { get; set; } = string.Empty; + + public int ItemsTotal { get; set; } + + public decimal TaxRate { get; set; } + + public decimal AmountPaid { get; set; } +} \ No newline at end of file diff --git a/ClosedXML.Report.Benchmarks/Program.cs b/ClosedXML.Report.Benchmarks/Program.cs new file mode 100644 index 0000000..4797db5 --- /dev/null +++ b/ClosedXML.Report.Benchmarks/Program.cs @@ -0,0 +1,58 @@ +using BenchmarkDotNet.Running; +using ClosedXML.Report.Benchmarks.Models; + +namespace ClosedXML.Report.Benchmarks; + +public class Program +{ + static void Main(string[] args) + { + BenchmarkRunner.Run(typeof(Program).Assembly); + } +} + +public class DataBuilder +{ + public Order CreateOrder(int orderNo = 1, int amount = 10000) + { + var orderNoStr = orderNo.ToString("D6"); + var isOdd = orderNo % 2 == 1; + + return new Order + { + AmountPaid = amount, + ItemsTotal = isOdd ? 1 : 2, + OrderNo = orderNoStr, + PaymentMethod = isOdd ? "Credit" : "Visa", + SaleDate = DateTime.Now, + ShipDate = DateTime.Now, + ShipToAddr1 = "ShipToAddr1", + ShipToAddr2 = "ShipToAddr2", + TaxRate = 0.1m + }; + } + + public Customer Create() + { + var c = new Customer + { + City = "City", + Addr1 = "1 Main St", + Addr2 = "Townsville", + Company = "Company Name", + Country = "Australia", + Email = "boss@gmail.com", + Fax = "0011 123 456 789", + Phone = "0011 123 456 789", + State = "Qld", + Zip = "4000", + Orders = [] + }; + for (var i = 0; i < 10000; i++) + { + c.Orders.Add(CreateOrder(i, 100 + i)); + } + + return c; + } +} \ No newline at end of file diff --git a/ClosedXML.Report.Benchmarks/Resources/Benchmark.xlsx b/ClosedXML.Report.Benchmarks/Resources/Benchmark.xlsx new file mode 100644 index 0000000..5be7377 Binary files /dev/null and b/ClosedXML.Report.Benchmarks/Resources/Benchmark.xlsx differ diff --git a/ClosedXML.Report.sln b/ClosedXML.Report.sln index fd5a12a..16b2e22 100644 --- a/ClosedXML.Report.sln +++ b/ClosedXML.Report.sln @@ -11,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8438BAA8 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClosedXML.Report.Tests", "tests\ClosedXML.Report.Tests\ClosedXML.Report.Tests.csproj", "{D529A371-3AEA-4D02-9A0D-308A6276D411}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClosedXML.Report.Benchmarks", "ClosedXML.Report.Benchmarks\ClosedXML.Report.Benchmarks.csproj", "{E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -25,6 +27,10 @@ Global {D529A371-3AEA-4D02-9A0D-308A6276D411}.Debug|Any CPU.Build.0 = Debug|Any CPU {D529A371-3AEA-4D02-9A0D-308A6276D411}.Release|Any CPU.ActiveCfg = Release|Any CPU {D529A371-3AEA-4D02-9A0D-308A6276D411}.Release|Any CPU.Build.0 = Release|Any CPU + {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -32,6 +38,7 @@ Global GlobalSection(NestedProjects) = preSolution {DEF4A219-D1CF-42A2-A5AC-FE7F2F005AB0} = {885EF47A-C124-45F0-B979-303B30FB586B} {D529A371-3AEA-4D02-9A0D-308A6276D411} = {8438BAA8-E968-4C14-9500-E3A52434E32A} + {E61B9B2F-96AF-4B19-9327-C7B724BA0B1D} = {8438BAA8-E968-4C14-9500-E3A52434E32A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {42DA0EC5-D733-4746-9613-413A64EEDC4B} diff --git a/ClosedXML.Report/Excel/CellPosition.cs b/ClosedXML.Report/Excel/CellPosition.cs new file mode 100644 index 0000000..1d48dde --- /dev/null +++ b/ClosedXML.Report/Excel/CellPosition.cs @@ -0,0 +1,14 @@ +namespace ClosedXML.Report.Excel; + +struct CellPosition +{ + public CellPosition(int row, int column) + { + Row = row; + Column = column; + } + + public int Row { get; set; } + + public int Column { get; set; } +} \ No newline at end of file diff --git a/ClosedXML.Report/Excel/Subtotal.cs b/ClosedXML.Report/Excel/Subtotal.cs index 76d0af5..9b2b052 100644 --- a/ClosedXML.Report/Excel/Subtotal.cs +++ b/ClosedXML.Report/Excel/Subtotal.cs @@ -298,7 +298,7 @@ public SubtotalGroup[] ScanForGroups(int groupBy) var result = new List(grRanges.Length); var rows = Sheet.Rows(_range.RangeAddress.FirstAddress.RowNumber, _range.RangeAddress.LastAddress.RowNumber); if (!rows.Any()) - return new SubtotalGroup[0]; + return Array.Empty(); var level = Math.Min(8, rows.Max(r => r.OutlineLevel) + 1); diff --git a/ClosedXML.Report/Excel/SubtotalGroup.cs b/ClosedXML.Report/Excel/SubtotalGroup.cs index c810162..75eca77 100644 --- a/ClosedXML.Report/Excel/SubtotalGroup.cs +++ b/ClosedXML.Report/Excel/SubtotalGroup.cs @@ -1,4 +1,3 @@ -using System; using ClosedXML.Excel; namespace ClosedXML.Report.Excel diff --git a/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs b/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs index 8d1bd51..f617395 100644 --- a/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs +++ b/ClosedXML.Report/Excel/SubtotalSummaryFunc.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq.Expressions; using ClosedXML.Report.Utils; namespace ClosedXML.Report.Excel { public class SubtotalSummaryFunc { - private static readonly Dictionary> TotalFuncs = new Dictionary> + private static readonly Dictionary> TotalFuncs = new() { {"average", new FuncData(1)}, {"avg", new FuncData(1)}, @@ -27,7 +26,7 @@ public class SubtotalSummaryFunc private static IFuncData GetFunc(string funcName) { - var func = TotalFuncs.ContainsKey(funcName) ? TotalFuncs[funcName] : null; + var func = TotalFuncs.TryGetValue(funcName, out var totalFunc) ? totalFunc : null; if (func == null) Debug.WriteLine("Unknown function " + funcName); return func; @@ -70,7 +69,6 @@ internal object Calculate(IDataSource dataSource) var agg = _func.CreateAggregator(); var dlg = GetCalculateDelegate(items[0].GetType()); - //var dlg = lambda.Compile(); foreach (var item in items) { try @@ -171,25 +169,25 @@ public void Aggregate(object value) private class CountAggregator : IAggregator { - private int _cnt = 0; + private int _cnt; public void Aggregate(object value) { if (value.GetType().IsNumeric()) _cnt++; } - public object Result { get { return _cnt; } } + public object Result => _cnt; } private class CountAAggregator : IAggregator { - private int _cnt = 0; + private int _cnt; public void Aggregate(object value) { _cnt++; } - public object Result { get { return _cnt; } } + public object Result => _cnt; } private class MinAggregator : IAggregator @@ -206,7 +204,7 @@ public void Aggregate(object value) _min = value; } - public object Result { get { return _min; } } + public object Result => _min; } private class MaxAggregator : IAggregator @@ -223,12 +221,12 @@ public void Aggregate(object value) _max = value; } - public object Result { get { return _max; } } + public object Result => _max; } private class AverageAggregator : IAggregator { - protected readonly List List = new List(); + protected readonly List List = new(); public void Aggregate(object value) { @@ -246,9 +244,9 @@ public virtual object Result foreach (dynamic v in List) sum += v; - if (sum is TimeSpan) + if (sum is TimeSpan span) { - return TimeSpan.FromTicks(((TimeSpan)sum).Ticks / List.Count); + return TimeSpan.FromTicks(span.Ticks / List.Count); } return sum / List.Count; diff --git a/ClosedXML.Report/Excel/TempSheetBuffer.cs b/ClosedXML.Report/Excel/TempSheetBuffer.cs index d34c0f8..dfa384a 100644 --- a/ClosedXML.Report/Excel/TempSheetBuffer.cs +++ b/ClosedXML.Report/Excel/TempSheetBuffer.cs @@ -10,12 +10,10 @@ internal class TempSheetBuffer: IReportBuffer private const string SheetName = "__temp_buffer"; private readonly XLWorkbook _wb; private IXLWorksheet _sheet; - private int _row; - private int _clmn; - private int _minRow; - private int _minClmn; - private int _prevrow; - private int _prevclmn; + private CellPosition _cellPosition; + private CellPosition _minCellPosition; + private CellPosition _prevCellPosition; + private CellPosition _maxCellPosition; public TempSheetBuffer(XLWorkbook wb) { @@ -23,28 +21,33 @@ public TempSheetBuffer(XLWorkbook wb) Init(); } - public IXLAddress NextAddress => _sheet.Cell(_row, _clmn).Address; - public IXLAddress PrevAddress => _sheet.Cell(_prevrow, _prevclmn).Address; + public IXLAddress NextAddress => _sheet.Cell(_cellPosition.Row, _cellPosition.Column).Address; + public IXLAddress PrevAddress => _sheet.Cell(_prevCellPosition.Row, _prevCellPosition.Column).Address; private void Init() { if (_sheet == null) { - if (!_wb.TryGetWorksheet(SheetName, out _sheet)) - { + if (!_wb.TryGetWorksheet(SheetName, out _sheet)) _sheet = _wb.AddWorksheet(SheetName); - } + _sheet.Visibility = XLWorksheetVisibility.VeryHidden; } - _row = _minRow = _prevrow = 1; - _clmn = _minClmn = _prevclmn = 1; + + _cellPosition = new CellPosition() + { + Row = _minCellPosition.Row = _prevCellPosition.Row = 1, + Column = _minCellPosition.Column = _prevCellPosition.Column = 1 + }; + _maxCellPosition.Row = _maxCellPosition.Column = 1; + Clear(); _sheet.Style = _wb.Worksheets.First().Style; } - public IXLCell WriteValue(object value, IXLCell settingCell) + public IXLCell WriteCellValue(object value, IXLCell settingCell) { - var xlCell = _sheet.Cell(_row, _clmn); + var xlCell = _sheet.Cell(_cellPosition.Row, _cellPosition.Column); if (settingCell != null) { xlCell.CopyFrom(settingCell); @@ -52,49 +55,59 @@ public IXLCell WriteValue(object value, IXLCell settingCell) try { - var cellValue = XLCellValueConverter.FromObject(value); - xlCell.SetValue(cellValue); + xlCell.SetValue(XLCellValueConverter.FromObject(value)); } catch (ArgumentException) { xlCell.SetValue(value?.ToString()); } - ChangeAddress(_row, _clmn + 1); + UpdateMaxAddress(_cellPosition); + ChangeAddress(_cellPosition.Row, _cellPosition.Column + 1); return xlCell; } + + private void UpdateMaxAddress(CellPosition cellPosition) + { + if (cellPosition.Row > _maxCellPosition.Row) _maxCellPosition.Row = cellPosition.Row; + if (cellPosition.Column > _maxCellPosition.Column) _maxCellPosition.Column = cellPosition.Column; + } public IXLCell WriteFormulaR1C1(string formula, IXLCell settingCell) { - var xlCell = _sheet.Cell(_row, _clmn); + var xlCell = _sheet.Cell(_cellPosition.Row, _cellPosition.Column); xlCell.CopyFrom(settingCell); xlCell.SetFormulaR1C1(formula); - ChangeAddress(_row, _clmn + 1); + UpdateMaxAddress(_cellPosition); + ChangeAddress(_cellPosition.Row, _cellPosition.Column + 1); return xlCell; } public void NewRow() { - if (_clmn > 1) - _clmn--; - ChangeAddress(_row + 1, _minClmn); - _minRow = _row; + StepBackColumn(); + ChangeAddress(_cellPosition.Row + 1, _minCellPosition.Column); + _minCellPosition.Row = _cellPosition.Row; + } + + private void StepBackColumn() + { + if (_cellPosition.Column > 1) + _cellPosition.Column--; } public void NewRow(IXLAddress startAddr) { - if (_clmn > 1) - _clmn--; - ChangeAddress(_row + 1, startAddr.ColumnNumber); - _minRow = _row; + StepBackColumn(); + ChangeAddress(_cellPosition.Row + 1, startAddr.ColumnNumber); + _minCellPosition.Row = _cellPosition.Row; } public void NewColumn(IXLAddress startAddr) { - if (_clmn > 1) - _clmn--; - ChangeAddress(startAddr.RowNumber, _clmn + 1); - _minClmn = _clmn; + StepBackColumn(); + ChangeAddress(startAddr.RowNumber, _cellPosition.Column + 1); + _minCellPosition.Column = _cellPosition.Column; } public IXLRange GetRange(IXLAddress startAddr, IXLAddress endAddr) @@ -102,23 +115,24 @@ public IXLRange GetRange(IXLAddress startAddr, IXLAddress endAddr) return _sheet.Range(startAddr, endAddr); } - public IXLCell GetCell(int row, int column) + public IXLCell GetCell(CellPosition cellPosition) { - return _sheet.Cell(row, column); + return _sheet.Cell(cellPosition.Row, cellPosition.Column); } - private void ChangeAddress(int row, int clmn) + private void ChangeAddress(int row, int column) { - _prevrow = _row; - _prevclmn = _clmn; - _row = row; - _clmn = clmn; + _prevCellPosition = _cellPosition; + _cellPosition = new CellPosition{ + Row = row, + Column = column + }; } public IXLRange CopyTo(IXLRange range) { var firstCell = _sheet.Cell(1, 1); - var tempRng = _sheet.Range(firstCell, LastCell); + var tempRng = _sheet.Range(firstCell, LastCellUsed); var rowDiff = tempRng.RowCount() - range.RowCount(); if (rowDiff > 0) @@ -152,7 +166,6 @@ public IXLRange CopyTo(IXLRange range) foreach (var picture in _sheet.Pictures) { var tgtPic = picture.CopyTo(tgtSheet); - //var relAddress = picture.TopLeftCell.Relative(range.RangeAddress.FirstAddress); var tgtCell = range.RangeAddress.FirstAddress.Offset(picture.TopLeftCell.Address); tgtPic.MoveTo(tgtCell); } @@ -170,23 +183,14 @@ public IXLRange CopyTo(IXLRange range) return range; } - public IXLCell LastCell - { - get - { - var rowNumber = Math.Max(_prevrow, _sheet.RowsUsed().LastOrDefault()?.RowNumber() ?? 1); - var columnNumber = Math.Max(_prevclmn, _sheet.ColumnsUsed().LastOrDefault()?.ColumnNumber() ?? 1); - var lastCell = GetCell(rowNumber, columnNumber); //_sheet.Cell(_prevrow, _prevclmn); - return lastCell; - } - } + public IXLCell LastCellUsed => GetCell(_maxCellPosition); public void SetPrevCellToLastUsed() { var lastUsed = _sheet.LastCellUsed(); - var clmn = _clmn < lastUsed.Address.ColumnNumber + var clmn = _cellPosition.Column < lastUsed.Address.ColumnNumber ? lastUsed.Address.ColumnNumber + 1 - : _clmn; + : _cellPosition.Column; ChangeAddress(lastUsed.Address.RowNumber, clmn); NewRow(); diff --git a/ClosedXML.Report/Excel/XlExtensions.cs b/ClosedXML.Report/Excel/XlExtensions.cs index 368a9dd..c43a7ff 100644 --- a/ClosedXML.Report/Excel/XlExtensions.cs +++ b/ClosedXML.Report/Excel/XlExtensions.cs @@ -118,7 +118,7 @@ internal static KeyValuePair[] GetRangeParameters(this } /// - /// Get the named ranges that contains the specified range (completely). + /// Get the named ranges that contain the specified range (completely). /// /// range public static IEnumerable GetContainingNames(this IXLRange range) @@ -214,9 +214,7 @@ public static void CopyStylesFrom(this IXLRangeBase trgtRow, IXLRangeBase srcRow var rela = srcCells[i].Relative(srcRow.RangeAddress.FirstAddress); var trgtCell = trgtRow.RangeAddress.FirstAddress.Offset(rela); trgtCell.Style = srcCells[i].Style; - //trgtCells[i].Style = srcCells[i].Style; } - //trgtRow.CopyConditionalFormatsFrom(srcRow); } public static void CopyFrom(this IXLConditionalFormat targetFormat, IXLConditionalFormat srcFormat) diff --git a/ClosedXML.Report/RangeTemplate.cs b/ClosedXML.Report/RangeTemplate.cs index 27a349c..a59f697 100644 --- a/ClosedXML.Report/RangeTemplate.cs +++ b/ClosedXML.Report/RangeTemplate.cs @@ -32,7 +32,7 @@ public class RangeTemplate private bool _isOptionsRowEmpty = true; private bool _isSubrange; private IDictionary _globalVariables; - + public string Source { get; private set; } public string Name { get; } @@ -84,12 +84,13 @@ private static RangeTemplate Parse(string name, IXLRange range, TempSheetBuffer var innerRanges = GetInnerRanges(range).ToArray(); var sheet = range.Worksheet; + var innerCells = innerRanges.SelectMany(x => x.Ranges.Cells()).ToHashSet(); for (int iRow = 1; iRow <= result._rowCnt; iRow++) { for (int iColumn = 1; iColumn <= result._colCnt; iColumn++) { var xlCell = range.Cell(iRow, iColumn); - if (innerRanges.Any(x => x.Ranges.Cells().Contains(xlCell))) + if (innerCells.Contains(xlCell)) xlCell = null; result._cells.Add(iRow, iColumn, xlCell); } @@ -97,7 +98,10 @@ private static RangeTemplate Parse(string name, IXLRange range, TempSheetBuffer result._cells.AddNewRow(); } - result._mergedRanges = sheet.MergedRanges.Where(x => range.Contains(x) && !innerRanges.Any(nr => nr.Ranges.Any(r => r.Contains(x)))).ToArray(); + result._mergedRanges = sheet.MergedRanges + .Where(range.Contains) + .Where(x => !innerRanges.Any(nr => nr.Ranges.Any(r => r.Contains(x)))) + .ToArray(); sheet.MergedRanges.RemoveAll(result._mergedRanges.Contains); result.ParseTags(range); @@ -174,13 +178,25 @@ public IReportBuffer Generate(object[] items) private void VerticalTable(object[] items, FormulaEvaluator evaluator) { var rangeStart = _buff.NextAddress; + + // Precompute merged ranges that do NOT belong to options row + var mergedRangesNonOptions = _mergedRanges.Where(r => !_optionsRow?.Contains(r) ?? true).ToArray(); + + // Precompute worksheet row heights once + var worksheet = _rowRange.Worksheet; + var rowHeightMap = _cells + .Where(c => c.XLCell != null && c.Row <= _rowCnt) + .GroupBy(c => c.XLCell.Address.RowNumber) + .ToDictionary(g => g.Key, g => worksheet.Row(g.Key).Height); + + for (int i = 0; i < items.Length; i++) { var startAddr = _buff.NextAddress; IXLAddress rowEnd = null; int row = 1; var tags = _tags.CopyTo(_rowRange); - var renderedSubranges = new List(); + var renderedSubranges = new HashSet(); // render row cells for (var iCell = 0; iCell < _cells.Count; iCell++) @@ -192,11 +208,10 @@ private void VerticalTable(object[] items, FormulaEvaluator evaluator) if (cell.CellType == TemplateCellType.None) { var xlCell = _rowRange.Cell(cell.Row, cell.Column); - var ownRng = _subranges.First(r => r._cells.Any(c => c.CellType != TemplateCellType.None && c.XLCell != null && Equals(c.XLCell.Address, xlCell.Address))); - if (!renderedSubranges.Contains(ownRng.Name)) + var ownRng = _subranges.FirstOrDefault(r => r._cells.Any(c => c.CellType != TemplateCellType.None && c.XLCell != null && Equals(c.XLCell.Address, xlCell.Address))); + if (ownRng != null && renderedSubranges.Add(ownRng.Name)) { RenderSubrange(ownRng, items[i], evaluator, cell, tags, ref iCell, ref row); - renderedSubranges.Add(ownRng.Name); } } else if (cell.CellType == TemplateCellType.NewRow) @@ -212,12 +227,11 @@ private void VerticalTable(object[] items, FormulaEvaluator evaluator) RenderCell(items, i, evaluator, cell); } } - + var newRowRng = _buff.GetRange(startAddr, rowEnd); - foreach (var mrg in _mergedRanges.Where(r => !_optionsRow.Contains(r))) + foreach (var mrg in mergedRangesNonOptions) { - var newMrg = mrg.Relative(_rowRange, newRowRng); - newMrg.Merge(false); + mrg.Relative(_rowRange, newRowRng).Merge(false); } tags.Execute(new ProcessingContext(newRowRng, items[i], evaluator)); @@ -240,24 +254,18 @@ private void VerticalTable(object[] items, FormulaEvaluator evaluator) var optionsRow = resultRange.LastRow().AsRange(); foreach (var mrg in _mergedRanges.Where(r => _optionsRow.Contains(r))) { - var newMrg = mrg.Relative(_optionsRow, optionsRow); - newMrg.Merge(); + mrg.Relative(_optionsRow, optionsRow).Merge(); } } - // arrage rows height - var worksheet = _rowRange.Worksheet; - var rowNumbers = _cells.Where(xc => xc.XLCell != null && xc.Row <= _rowCnt) - .Select(xc => xc.XLCell.Address.RowNumber) - .Distinct() - .ToArray(); - var heights = rowNumbers - .Select(c => worksheet.Row(c).Height) - .ToArray(); - var firstRow = rowNumbers.Min(); - foreach (var row in Enumerable.Range(rangeStart.RowNumber, _buff.PrevAddress.RowNumber)) + // arrange rows height + var firstRow = rowHeightMap.Keys.Min(); + var heights = rowHeightMap.Values.ToArray(); + var heightsLength = heights.Length; + + for (int rowNum = rangeStart.RowNumber; rowNum <= _buff.PrevAddress.RowNumber; rowNum++) { - worksheet.Row(firstRow + row - 1).Height = heights[(row - 1) % heights.Length]; + worksheet.Row(firstRow + rowNum - 1).Height = heights[(rowNum - 1) % heightsLength]; } if (_isSubrange) @@ -272,7 +280,7 @@ private void RenderCell(FormulaEvaluator evaluator, TemplateCell cell, params Pa { if (cell.CellType != TemplateCellType.Formula && cell.CellType != TemplateCellType.Value) { - _buff.WriteValue(null, null); + _buff.WriteCellValue(null, null); return; } @@ -285,9 +293,7 @@ private void RenderCell(FormulaEvaluator evaluator, TemplateCell cell, params Pa } catch (ParseException ex) { - _buff.WriteValue(ex.Message, cell.XLCell); - _buff.GetCell(_buff.PrevAddress.RowNumber, _buff.PrevAddress.ColumnNumber).Style.Font.FontColor = XLColor.Red; - _errors.Add(new TemplateError(ex.Message, cell.XLCell.AsRange())); + HandleError(ex.Message, cell); return; } catch (TargetInvocationException) @@ -308,21 +314,14 @@ private void RenderCell(FormulaEvaluator evaluator, TemplateCell cell, params Pa * just add to the error list for future use and keep doing the work, other items may have the material property. * No need to write the error in the cell since it might be a desired behaviour, but needs to go to next cell. */ - _buff.WriteValue(string.Empty, cell.XLCell); - _errors.Add(new TemplateError(string.Format("TargetInvocationException: {0}", cell.Value), cell.XLCell.AsRange())); + _buff.WriteCellValue(string.Empty, cell.XLCell); + _errors.Add(new TemplateError($"TargetInvocationException: {cell.Value}", cell.XLCell.AsRange())); return; } - IXLCell xlCell; - if (cell.CellType == TemplateCellType.Formula) - { - var r1c1 = cell.XLCell.GetFormulaR1C1(value.ToString()); - xlCell = _buff.WriteFormulaR1C1(r1c1, cell.XLCell); - } - else - { - xlCell = _buff.WriteValue(value, cell.XLCell); - } + IXLCell xlCell = cell.CellType == TemplateCellType.Formula + ? _buff.WriteFormulaR1C1(cell.XLCell.GetFormulaR1C1(value.ToString()), cell.XLCell) + : _buff.WriteCellValue(value, cell.XLCell); string EvalString(string str) { @@ -346,10 +345,11 @@ string EvalString(string str) if (xlCell.HasHyperlink) { - if (xlCell.GetHyperlink().IsExternal) - xlCell.GetHyperlink().ExternalAddress = new Uri(EvalString(xlCell.GetHyperlink().ExternalAddress.ToString())); + var link = xlCell.GetHyperlink(); + if (link.IsExternal) + link.ExternalAddress = new Uri(EvalString(link.ExternalAddress.ToString())); else - xlCell.GetHyperlink().InternalAddress = EvalString(xlCell.GetHyperlink().InternalAddress); + link.InternalAddress = EvalString(link.InternalAddress); } if (xlCell.HasRichText) @@ -359,8 +359,13 @@ string EvalString(string str) xlCell.GetRichText().AddText(richText); } } - - + + private void HandleError(string message, TemplateCell cell) + { + _buff.WriteCellValue(message, cell.XLCell); + _buff.GetCell(new CellPosition(_buff.PrevAddress.RowNumber, _buff.PrevAddress.ColumnNumber)).Style.Font.FontColor = XLColor.Red; + _errors.Add(new TemplateError(message, cell.XLCell.AsRange())); + } private void RenderCell(object[] items, int i, FormulaEvaluator evaluator, TemplateCell cell) { @@ -381,17 +386,20 @@ private void RenderSubrange(RangeTemplate subrange, object item, FormulaEvaluato if (subrange.IsHorizontal) { - int shiftLen = subrange._colCnt * (valArr.Length - 1); - tags.Where(tag => tag.Cell.Row == cell.Row && tag.Cell.Column > cell.Column) - .ForEach(t => + var shiftLen = subrange._colCnt * (valArr.Length - 1); + if (shiftLen > 0) + { + var tagsToShift = tags.Where(tag => tag.Cell.Row == cell.Row && tag.Cell.Column > cell.Column).ToArray(); + foreach (var t in tagsToShift) { t.Cell.Column += shiftLen; t.Cell.XLCell = _rowRange.Cell(t.Cell.Row, t.Cell.Column); - }); + } + } } else { - // move current template cell to next (skip subrange) + // move the current template cell to next (skip subrange) row += subrange._rowCnt + 1; while (_cells[iCell].Row <= row - 1) iCell++; @@ -458,19 +466,19 @@ private void HorizontalTable(object[] items, FormulaEvaluator evaluator) { worksheet.Column(firstCol + col - 1).Width = widths[(col - 1) % widths.Length]; } - - /*using (var resultRange = _buff.GetRange(rangeStart, _buff.PrevAddress)) - _rangeTags.Execute(new ProcessingContext(resultRange, new DataSource(items)));*/ } private void ParseTags(IXLRange range) { var innerRanges = range.GetContainingNames().ToArray(); - var cells = from c in _cells - let value = c.GetString() - where TagExtensions.HasTag(value) - && !innerRanges.Any(nr => nr.Ranges.Contains(c.XLCell.AsRange())) - select c; + var innerRangeCells = innerRanges + .SelectMany(nr => nr.Ranges) + .SelectMany(r => r.Cells()) + .ToHashSet(); + + var cells = _cells + .Where(c => c.XLCell != null && !innerRangeCells.Contains(c.XLCell)) + .Where(c => TagExtensions.HasTag(c.GetString())); foreach (var cell in cells) { diff --git a/ClosedXML.Report/XLTemplate.cs b/ClosedXML.Report/XLTemplate.cs index 04283e3..d12d852 100644 --- a/ClosedXML.Report/XLTemplate.cs +++ b/ClosedXML.Report/XLTemplate.cs @@ -118,8 +118,8 @@ public void AddVariable(object value) public void AddVariable(string alias, object value) { CheckIsDisposed(); - if (value is DataTable) - value = ((DataTable) value).Rows.Cast(); + if (value is DataTable table) + value = table.Rows.Cast(); _interpreter.AddVariable(alias, value); } diff --git a/tests/ClosedXML.Report.Tests/GroupTagTests.cs b/tests/ClosedXML.Report.Tests/GroupTagTests.cs index 130a07c..15c03da 100644 --- a/tests/ClosedXML.Report.Tests/GroupTagTests.cs +++ b/tests/ClosedXML.Report.Tests/GroupTagTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using ClosedXML.Report.Tests.TestModels; using LinqToDB; using Xunit; @@ -62,7 +63,7 @@ public void Simple(string templateFile) public void EmptyDataSource(string templateFile) { XlTemplateTest(templateFile, - tpl => tpl.AddVariable("Orders", new Order[0]), + tpl => tpl.AddVariable("Orders", Array.Empty()), wb => { }); } diff --git a/tests/ClosedXML.Report.Tests/OnlyValuesTagTests.cs b/tests/ClosedXML.Report.Tests/OnlyValuesTagTests.cs index 6133451..1840056 100644 --- a/tests/ClosedXML.Report.Tests/OnlyValuesTagTests.cs +++ b/tests/ClosedXML.Report.Tests/OnlyValuesTagTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using ClosedXML.Excel; using ClosedXML.Report.Options; using FluentAssertions; @@ -13,7 +14,7 @@ public void TagInA2CellShouldReplaceAllFormulasOnWorksheet() { FillData(); var tag = CreateNotInRangeTag(_ws.Cell("A2")); - tag.Execute(new ProcessingContext(_ws.AsRange(), new DataSource(new object[0]), new FormulaEvaluator())); + tag.Execute(new ProcessingContext(_ws.AsRange(), new DataSource(Array.Empty()), new FormulaEvaluator())); _ws.CellsUsed(c => c.HasFormula).Should().BeEmpty(); } @@ -24,7 +25,7 @@ public void TagInFirstCellRangeOptionRowShouldReplaceAllFormulasInRange() var rng = FillData(); var tag = CreateInRangeTag(rng, rng.Cell(2, 1)); - tag.Execute(new ProcessingContext(_ws.Range("B5", "F15"), new DataSource(new object[0]), new FormulaEvaluator())); + tag.Execute(new ProcessingContext(_ws.Range("B5", "F15"), new DataSource(Array.Empty()), new FormulaEvaluator())); rng.CellsUsed(c => c.HasFormula).Should().BeEmpty(); _ws.Cell("B3").HasFormula.Should().BeTrue(); @@ -37,7 +38,7 @@ public void TagInRangeCellShouldReplaceAllFormulasOnlyThisColumnInRange() var dataRng = _ws.Range("B5", "D7"); var tag = CreateInRangeTag(rng, rng.Cell(1, 2)); - tag.Execute(new ProcessingContext(dataRng, new DataSource(new object[0]), new FormulaEvaluator())); + tag.Execute(new ProcessingContext(dataRng, new DataSource(Array.Empty()), new FormulaEvaluator())); dataRng.Column(1).Cells(c => c.HasFormula).Count().Should().Be(3); dataRng.Column(2).Cells(c => c.HasFormula).Should().BeEmpty(); diff --git a/tests/ClosedXML.Report.Tests/TempSheetBufferTests.cs b/tests/ClosedXML.Report.Tests/TempSheetBufferTests.cs index 36026cc..3b981c3 100644 --- a/tests/ClosedXML.Report.Tests/TempSheetBufferTests.cs +++ b/tests/ClosedXML.Report.Tests/TempSheetBufferTests.cs @@ -18,8 +18,8 @@ public void NamedRangesAreRemovedWithTempSheet() var tempSheetBuffer = new TempSheetBuffer(wb); wb.DefinedNames.Add("Temp range", tempSheetBuffer.GetRange( - tempSheetBuffer.GetCell(1, 1).Address, - tempSheetBuffer.GetCell(4, 4).Address)); + tempSheetBuffer.GetCell(new CellPosition(1, 1)).Address, + tempSheetBuffer.GetCell(new CellPosition(4, 4)).Address)); wb.DefinedNames.Count().Should().Be(1, "global named range is supposed to be added"); tempSheetBuffer.Dispose(); diff --git a/tests/Gauges/tLists7_horizontal_images.xlsx b/tests/Gauges/tLists7_horizontal_images.xlsx index 90633b2..398e4c4 100644 Binary files a/tests/Gauges/tLists7_horizontal_images.xlsx and b/tests/Gauges/tLists7_horizontal_images.xlsx differ