Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 45 additions & 21 deletions Core/Aggregation/NumericAggregator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Reflection;
using System.Collections.Concurrent;
using System.Linq.Expressions;

namespace ExcelGenerator.Core.Aggregation;

Expand All @@ -8,54 +10,72 @@ namespace ExcelGenerator.Core.Aggregation;
/// </summary>
internal class NumericAggregator
{
private static readonly ConcurrentDictionary<PropertyInfo, Func<object, object>> _propertyAccessorCache = new();

/// <summary>
/// Gets or creates a compiled property accessor for better performance
/// </summary>
private static Func<object, object> GetPropertyAccessor(PropertyInfo property)
{
return _propertyAccessorCache.GetOrAdd(property, prop =>
{
var parameter = Expression.Parameter(typeof(object), "obj");
var cast = Expression.Convert(parameter, prop.DeclaringType!);
var propertyAccess = Expression.Property(cast, prop);
var convertToObject = Expression.Convert(propertyAccess, typeof(object));
return Expression.Lambda<Func<object, object>>(convertToObject, parameter).Compile();
});
}

/// <summary>
/// Calculates sum for the specified numeric type
/// </summary>
public static double CalculateSum<T>(List<T> dataList, PropertyInfo property, Type underlyingType)
{
var accessor = GetPropertyAccessor(property);
if (underlyingType == typeof(decimal))
{
var sum = dataList
.Select(item => item == null ? 0m : (decimal)(property.GetValue(item) ?? 0m))
.Select(item => item == null ? 0m : (decimal)(accessor(item) ?? 0m))
.Sum();
return (double)sum.RefineValue();
}
else if (underlyingType == typeof(double))
{
var sum = dataList
.Select(item => item == null ? 0.0 : (double)(property.GetValue(item) ?? 0.0))
.Select(item => item == null ? 0.0 : (double)(accessor(item) ?? 0.0))
.Sum();
return (double)((decimal)sum).RefineValue();
}
else if (underlyingType == typeof(float))
{
var sum = dataList
.Select(item => item == null ? 0f : (float)(property.GetValue(item) ?? 0f))
.Select(item => item == null ? 0f : (float)(accessor(item) ?? 0f))
.Sum();
return (double)((decimal)sum).RefineValue();
}
else if (underlyingType == typeof(int))
{
return dataList
.Select(item => item == null ? 0 : (int)(property.GetValue(item) ?? 0))
.Select(item => item == null ? 0 : (int)(accessor(item) ?? 0))
.Sum();
}
else if (underlyingType == typeof(long))
{
return dataList
.Select(item => item == null ? 0L : (long)(property.GetValue(item) ?? 0L))
.Select(item => item == null ? 0L : (long)(accessor(item) ?? 0L))
.Sum();
}
else if (underlyingType == typeof(short))
{
return dataList
.Select(item => item == null ? 0 : (int)(short)(property.GetValue(item) ?? (short)0))
.Select(item => item == null ? 0 : (int)(short)(accessor(item) ?? (short)0))
.Sum();
}
else if (underlyingType == typeof(byte))
{
return dataList
.Select(item => item == null ? 0 : (int)(byte)(property.GetValue(item) ?? (byte)0))
.Select(item => item == null ? 0 : (int)(byte)(accessor(item) ?? (byte)0))
.Sum();
}

Expand All @@ -67,49 +87,51 @@ public static double CalculateSum<T>(List<T> dataList, PropertyInfo property, Ty
/// </summary>
public static double CalculateMin<T>(List<T> dataList, PropertyInfo property, Type underlyingType)
{
var accessor = GetPropertyAccessor(property);

if (underlyingType == typeof(decimal))
{
var min = dataList
.Select(item => item == null ? decimal.MaxValue : (decimal)(property.GetValue(item) ?? decimal.MaxValue))
.Select(item => item == null ? decimal.MaxValue : (decimal)(accessor(item) ?? decimal.MaxValue))
.Min();
return (double)min.RefineValue();
}
else if (underlyingType == typeof(double))
{
var min = dataList
.Select(item => item == null ? double.MaxValue : (double)(property.GetValue(item) ?? double.MaxValue))
.Select(item => item == null ? double.MaxValue : (double)(accessor(item) ?? double.MaxValue))
.Min();
return (double)((decimal)min).RefineValue();
}
else if (underlyingType == typeof(float))
{
var min = dataList
.Select(item => item == null ? float.MaxValue : (float)(property.GetValue(item) ?? float.MaxValue))
.Select(item => item == null ? float.MaxValue : (float)(accessor(item) ?? float.MaxValue))
.Min();
return (double)((decimal)min).RefineValue();
}
else if (underlyingType == typeof(int))
{
return dataList
.Select(item => item == null ? int.MaxValue : (int)(property.GetValue(item) ?? int.MaxValue))
.Select(item => item == null ? int.MaxValue : (int)(accessor(item) ?? int.MaxValue))
.Min();
}
else if (underlyingType == typeof(long))
{
return dataList
.Select(item => item == null ? long.MaxValue : (long)(property.GetValue(item) ?? long.MaxValue))
.Select(item => item == null ? long.MaxValue : (long)(accessor(item) ?? long.MaxValue))
.Min();
}
else if (underlyingType == typeof(short))
{
return dataList
.Select(item => item == null ? short.MaxValue : (int)(short)(property.GetValue(item) ?? short.MaxValue))
.Select(item => item == null ? short.MaxValue : (int)(short)(accessor(item) ?? short.MaxValue))
.Min();
}
else if (underlyingType == typeof(byte))
{
return dataList
.Select(item => item == null ? byte.MaxValue : (int)(byte)(property.GetValue(item) ?? byte.MaxValue))
.Select(item => item == null ? byte.MaxValue : (int)(byte)(accessor(item) ?? byte.MaxValue))
.Min();
}

Expand All @@ -121,49 +143,51 @@ public static double CalculateMin<T>(List<T> dataList, PropertyInfo property, Ty
/// </summary>
public static double CalculateMax<T>(List<T> dataList, PropertyInfo property, Type underlyingType)
{
var accessor = GetPropertyAccessor(property);

if (underlyingType == typeof(decimal))
{
var max = dataList
.Select(item => item == null ? decimal.MinValue : (decimal)(property.GetValue(item) ?? decimal.MinValue))
.Select(item => item == null ? decimal.MinValue : (decimal)(accessor(item) ?? decimal.MinValue))
.Max();
return (double)max.RefineValue();
}
else if (underlyingType == typeof(double))
{
var max = dataList
.Select(item => item == null ? double.MinValue : (double)(property.GetValue(item) ?? double.MinValue))
.Select(item => item == null ? double.MinValue : (double)(accessor(item) ?? double.MinValue))
.Max();
return (double)((decimal)max).RefineValue();
}
else if (underlyingType == typeof(float))
{
var max = dataList
.Select(item => item == null ? float.MinValue : (float)(property.GetValue(item) ?? float.MinValue))
.Select(item => item == null ? float.MinValue : (float)(accessor(item) ?? float.MinValue))
.Max();
return (double)((decimal)max).RefineValue();
}
else if (underlyingType == typeof(int))
{
return dataList
.Select(item => item == null ? int.MinValue : (int)(property.GetValue(item) ?? int.MinValue))
.Select(item => item == null ? int.MinValue : (int)(accessor(item) ?? int.MinValue))
.Max();
}
else if (underlyingType == typeof(long))
{
return dataList
.Select(item => item == null ? long.MinValue : (long)(property.GetValue(item) ?? long.MinValue))
.Select(item => item == null ? long.MinValue : (long)(accessor(item) ?? long.MinValue))
.Max();
}
else if (underlyingType == typeof(short))
{
return dataList
.Select(item => item == null ? short.MinValue : (int)(short)(property.GetValue(item) ?? short.MinValue))
.Select(item => item == null ? short.MinValue : (int)(short)(accessor(item) ?? short.MinValue))
.Max();
}
else if (underlyingType == typeof(byte))
{
return dataList
.Select(item => item == null ? byte.MinValue : (int)(byte)(property.GetValue(item) ?? byte.MinValue))
.Select(item => item == null ? byte.MinValue : (int)(byte)(accessor(item) ?? byte.MinValue))
.Max();
}

Expand Down
17 changes: 13 additions & 4 deletions Core/CellFormatters/CellFormatterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,18 @@ public void FormatCell(IXLCell cell, object? value, Type type)
/// </summary>
private ICellValueFormatter GetFormatter(Type type)
{
return _formatters
.Where(f => f.CanFormat(type))
.OrderByDescending(f => f.Priority)
.FirstOrDefault() ?? _fallbackFormatter;
ICellValueFormatter? bestFormatter = null;
int highestPriority = int.MinValue;

foreach (var formatter in _formatters)
{
if (formatter.CanFormat(type) && formatter.Priority > highestPriority)
{
bestFormatter = formatter;
highestPriority = formatter.Priority;
}
}

return bestFormatter ?? _fallbackFormatter;
}
}
23 changes: 17 additions & 6 deletions Core/ExcelGeneratorEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using ExcelGenerator.Core.PropertyReflection;
using ExcelGenerator.Core.Generators;
using ExcelGenerator.Core.ConditionalFormatting;
using System.Text;

namespace ExcelGenerator.Core;

Expand Down Expand Up @@ -102,11 +103,18 @@ public XLWorkbook Generate<T>(
private void ApplyConditionalFormatting(IXLWorksheet worksheet, System.Reflection.PropertyInfo[] properties,
int dataCount, ConditionalFormattingConfiguration config)
{
// Build a dictionary for O(1) property name lookups
var propertyIndexMap = new Dictionary<string, int>(properties.Length);
for (int i = 0; i < properties.Length; i++)
{
propertyIndexMap[properties[i].Name] = i;
}

foreach (var rule in config.Rules)
{
// Find the column index for this property
var colIndex = Array.FindIndex(properties, p => p.Name == rule.ColumnName);
if (colIndex < 0) continue;
// Find the column index for this property using dictionary lookup
if (!propertyIndexMap.TryGetValue(rule.ColumnName, out var colIndex))
continue;

var columnLetter = GetColumnLetter(colIndex + 1);
var dataRange = worksheet.Range($"{columnLetter}2:{columnLetter}{dataCount + 1}");
Expand All @@ -119,14 +127,17 @@ private void ApplyConditionalFormatting(IXLWorksheet worksheet, System.Reflectio

private static string GetColumnLetter(int columnNumber)
{
string columnName = "";
var chars = new char[7]; // Excel max is XFD (col 16384) = 3 chars; 7 provides safe buffer
int index = chars.Length;

while (columnNumber > 0)
{
int modulo = (columnNumber - 1) % 26;
columnName = Convert.ToChar('A' + modulo) + columnName;
chars[--index] = (char)('A' + modulo);
columnNumber = (columnNumber - modulo) / 26;
}
return columnName;

return new string(chars, index, chars.Length - index);
}

/// <summary>
Expand Down
25 changes: 16 additions & 9 deletions Core/PropertyReflection/PropertyExtractor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Reflection;
using System.Text.RegularExpressions;
using System.Collections.Concurrent;

namespace ExcelGenerator.Core.PropertyReflection;

Expand All @@ -8,19 +9,25 @@ namespace ExcelGenerator.Core.PropertyReflection;
/// </summary>
internal class PropertyExtractor : IPropertyExtractor
{
private static readonly ConcurrentDictionary<(Type, bool), PropertyInfo[]> _propertyCache = new();

public PropertyInfo[] Extract<T>(bool excludeIds = false)
{
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead);

if (excludeIds)
var key = (typeof(T), excludeIds);
return _propertyCache.GetOrAdd(key, _ =>
{
properties = properties.Where(p =>
!p.Name.EndsWith("Id", StringComparison.OrdinalIgnoreCase) &&
!p.Name.EndsWith("ID", StringComparison.Ordinal));
}
var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead);

if (excludeIds)
{
properties = properties.Where(p =>
!p.Name.EndsWith("Id", StringComparison.OrdinalIgnoreCase) &&
!p.Name.EndsWith("ID", StringComparison.Ordinal));
}

return properties.ToArray();
return properties.ToArray();
});
}

public string FormatPropertyName(string propertyName)
Expand Down