diff --git a/src/EPPlus/ExcelWorkbook.cs b/src/EPPlus/ExcelWorkbook.cs
index 02b0c79d7..282f47ff6 100644
--- a/src/EPPlus/ExcelWorkbook.cs
+++ b/src/EPPlus/ExcelWorkbook.cs
@@ -439,7 +439,7 @@ internal void ThrowIfCalculationCanceled()
///
/// Returns true if a calculation was canceled, leaving the workbook in an inconsistent state.
- /// A workbook in this state must be disposed saving or recalculating is not permitted.
+ /// A workbook in this state must be disposed ïżœ saving or recalculating is not permitted.
///
public bool IsCalculationInconsistent => IsCalculationCanceled;
#endif
@@ -1196,14 +1196,6 @@ private void CreateWorkbookXml(XmlNamespaceManager namespaceManager)
XmlElement workbookView = _workbookXml.CreateElement("workbookView", ExcelPackage.schemaMain);
bookViews.AppendChild(workbookView);
- XmlElement calcPr = _workbookXml.CreateElement("calcPr", ExcelPackage.schemaMain);
- calcPr.SetAttribute("calcId", "191029"); //Set the version of the calc engine to the latest known version. This will make sure that Excel does not downgrade the calculation engine and that new functions are supported.
- wbElem.AppendChild(calcPr);
-
- XmlElement extLst = _workbookXml.CreateElement("extLst", ExcelPackage.schemaMain);
- AddCalculationFeatures(extLst);
- wbElem.AppendChild(extLst);
-
// save it to the package
StreamWriter stream = new StreamWriter(partWorkbook.GetStream(FileMode.Create, FileAccess.Write));
_workbookXml.Save(stream);
@@ -1399,7 +1391,8 @@ public ExcelCalcMode CalcMode
SetXmlNodeString(CALC_MODE_PATH, "autoNoTable");
break;
case ExcelCalcMode.Manual:
- SetXmlNodeString(CALC_MODE_PATH, "manual");
+ SetXmlNodeString(CALC_MODE_PATH, "manual");
+ SetXmlNodeString("d:calcPr/@calcId", "191029");
break;
default:
SetXmlNodeString(CALC_MODE_PATH, "auto");
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/BuiltInFunctions.cs b/src/EPPlus/FormulaParsing/Excel/Functions/BuiltInFunctions.cs
index 7108e5c99..e75d975e3 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/BuiltInFunctions.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/BuiltInFunctions.cs
@@ -17,6 +17,7 @@ Date Author Change
using OfficeOpenXml.FormulaParsing.Excel.Functions.DateAndTime;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Numeric;
using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup;
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.PivotBy;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Information;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Finance;
using OfficeOpenXml.FormulaParsing.Excel.Functions.Engineering;
@@ -336,7 +337,8 @@ public BuiltInFunctions()
// Reference and lookup
Functions["address"] = new Address();
Functions["areas"] = new Areas();
- //Functions["groupby"] = new GroupBy(); //Will be released in next minor release.
+ Functions["groupby"] = new GroupBy();
+ Functions["pivotby"] = new PivotBy();
Functions["hlookup"] = new HLookup();
Functions["vlookup"] = new VLookup();
Functions["xlookup"] = new Xlookup();
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/ColEntry.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/ColEntry.cs
new file mode 100644
index 000000000..2d582ae60
--- /dev/null
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/ColEntry.cs
@@ -0,0 +1,26 @@
+ï»ż/*************************************************************************************************
+ Required Notice: Copyright (C) EPPlus Software AB.
+ This software is licensed under PolyForm Noncommercial License 1.0.0
+ and may only be used for noncommercial purposes
+ https://polyformproject.org/licenses/noncommercial/1.0.0/
+
+ A commercial license to use this software can be purchased at https://epplussoftware.com
+ *************************************************************************************************
+ Date Author Change
+ *************************************************************************************************
+ 13/4/2026 EPPlus Software AB EPPlus v8.6
+ *************************************************************************************************/
+
+using OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.GroupingFunctions;
+using System.Collections.Generic;
+
+namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup
+{
+ internal class ColEntry
+ {
+ public bool IsSubtotal { get; set; }
+ public string GroupKey { get; set; }
+ public List GroupLeaves { get; set; }
+ public LeafWithPath Leaf { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Groupby.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Groupby.cs
index 8785bcd6e..7a13f2431 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Groupby.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/Groupby.cs
@@ -34,7 +34,7 @@ internal class GroupBy : GroupByFunctionBase
public override CompileResult Execute(IList arguments, ParsingContext context)
{
- if (!TryParseBaseArgs(arguments, out var args, out var error))
+ if (!TryParseGroupByArgs(arguments, out var args, out var error))
return error;
var groups = BuildGroups(args, context);
groups = ApplySort(groups, args);
@@ -43,112 +43,85 @@ public override CompileResult Execute(IList arguments, Parsing
return CreateDynamicArrayResult(result, DataType.ExcelRange);
}
- // -------------------------------------------------------
- // Sorting
- // -------------------------------------------------------
- private List ApplySort(List levels, GroupByBaseArgs args, int depth = 1)
+ private bool TryParseGroupByArgs(IList arguments,
+ out GroupByBaseArgs args,
+ out CompileResult error)
{
- if (args.SortOrders == null || args.SortOrders.All(s => s == 0)) return levels;
+ args = new GroupByBaseArgs();
+ error = null;
- if (args.FieldRelationship == FieldRelationship.Table)
- {
- var allRows = levels.SelectMany(l => CollectLeafRows(l)).ToList();
- allRows = SortRowsMulti(allRows, args);
+ if (!arguments[0].IsExcelRange) // TODO. Man kan skicka in enskilda celler i rowfields och values, sÄ detta Àr fel.
+ return Fail(eErrorType.Value, out error);
+ args.RowFields = arguments[0].ValueAsRangeInfo;
- var newLevelDict = new Dictionary();
- var newLevelOrder = new List();
- foreach (var row in allRows)
- {
- var topKey = (row.KeyParts[0]?.ToString() ?? string.Empty).ToLowerInvariant();
- if (!newLevelDict.TryGetValue(topKey, out var level))
- {
- level = new GroupLevel { Key = row.KeyParts[0] };
- newLevelDict[topKey] = level;
- newLevelOrder.Add(topKey);
- }
- level.Rows.Add(row);
- }
- return newLevelOrder.Select(k => newLevelDict[k]).ToList();
- }
- else
- {
- var sortForThisLevel = args.SortOrders
- .FirstOrDefault(s => Math.Abs(s) == depth);
+ if (!arguments[1].IsExcelRange)
+ return Fail(eErrorType.Value, out error);
+ args.Values = arguments[1].ValueAsRangeInfo;
- bool hasSortForThisLevel = sortForThisLevel != 0;
- bool desc = sortForThisLevel < 0;
- bool sortOnAggregated = hasSortForThisLevel && Math.Abs(sortForThisLevel) > args.RowFields.Size.NumberOfCols;
+ if (args.RowFields.Size.NumberOfRows != args.Values.Size.NumberOfRows)
+ return Fail(eErrorType.Value, out error);
- if (hasSortForThisLevel)
- {
- levels = sortOnAggregated
- ? (desc ? levels.OrderByDescending(l => l.SubtotalValue as IComparable, _comparer).ToList()
- : levels.OrderBy(l => l.SubtotalValue as IComparable, _comparer).ToList())
- : (desc ? levels.OrderByDescending(l => l.Key as IComparable, _comparer).ToList()
- : levels.OrderBy(l => l.Key as IComparable, _comparer).ToList());
- }
+ if (!TryParseFunctionArg(arguments[2], args.Functions, out LambdaCalculator function, out FunctionLayout layout))
+ return Fail(eErrorType.Value, out error);
- foreach (var level in levels)
- {
- if (!level.IsLeaf)
- level.Children = ApplySort(level.Children, args, depth + 1);
- else
- level.Rows = SortRowsMulti(level.Rows, args);
- }
+ args.Function = function;
+ args.FunctionLayout = layout;
+
+ if (args.Functions.Count == 0)
+ return Fail(eErrorType.Value, out error);
+
+ // field_headers (optional)
+ if (arguments.Count > 3 && arguments[3].Value != null)
+ {
+ var v = Convert.ToInt32(arguments[3].Value);
+ if (!Enum.IsDefined(typeof(FieldHeaders), v))
+ return Fail(eErrorType.Value, out error);
+ args.Headers = (FieldHeaders)v;
+ }
+ else if (args.Functions.Count > 1) // In excel, if multiple functions are included, headers are by default displayed.
+ {
+ args.Headers = FieldHeaders.YesAndShow;
+ }
- return levels;
+ // total_depth (optional)
+ if (arguments.Count > 4 && arguments[4].Value != null)
+ {
+ if (!TryParseTotalDepthArg(arguments[4], args.RowFields.Size.NumberOfCols, out int totalDepth))
+ return Fail(eErrorType.Value, out error);
+ args.TotalDepth = totalDepth;
}
- }
- private List SortRowsMulti(List rows, GroupByBaseArgs args)
- {
- if (rows == null || rows.Count == 0) return rows;
+ // sort_order (optional)
+ if (arguments.Count > 5 && arguments[5].Value != null)
+ {
+ args.SortOrders = ParseSortOrderArg(arguments[5]);
+ }
- int nKeyCols = args.RowFields.Size.NumberOfCols;
- IOrderedEnumerable ordered = null;
+ // filter_array (optional)
+ if (arguments.Count > 6 && arguments[6].IsExcelRange)
+ args.FilterArray = arguments[6].ValueAsRangeInfo;
- foreach (var sortOrder in args.SortOrders)
+ // field_relationship (optional)
+ if (arguments.Count > 7 && arguments[7].Value != null)
{
- if (sortOrder == 0) continue;
- bool desc = sortOrder < 0;
- int col = Math.Abs(sortOrder);
- bool sortOnAggregated = col > nKeyCols;
-
- // Capture loop variables
- var capturedCol = col;
- var capturedSortOnAggregated = sortOnAggregated;
-
- Func keySelector = capturedSortOnAggregated
- ? (Func)(r => r.AggregatedValue)
- : (r => r.KeyParts[Math.Min(capturedCol - 1, r.KeyParts.Length - 1)]);
-
- if (ordered == null)
- ordered = desc
- ? rows.OrderByDescending(keySelector, _comparer)
- : rows.OrderBy(keySelector, _comparer);
- else
- ordered = desc
- ? ordered.ThenByDescending(keySelector, _comparer)
- : ordered.ThenBy(keySelector, _comparer);
+ var v = Convert.ToInt32(arguments[7].Value);
+ if (!Enum.IsDefined(typeof(FieldRelationship), v))
+ return Fail(eErrorType.Value, out error);
+ if (v == (int)FieldRelationship.Table && Math.Abs(args.TotalDepth) > 1)
+ return Fail(eErrorType.Value, out error);
+ args.FieldRelationship = (FieldRelationship)v;
}
- return ordered?.ToList() ?? rows;
- }
-
- private IEnumerable CollectLeafRows(GroupLevel level)
- {
- if (level.IsLeaf)
- return level.Rows;
- return level.Children.SelectMany(c => CollectLeafRows(c));
+ return true;
}
-
+
// -------------------------------------------------------
// Build result
// -------------------------------------------------------
private InMemoryRange BuildResult(List levels, GroupByBaseArgs args, ParsingContext context)
{
- var resolvedHeaders = ResolveHeaders(args);
+ var resolvedHeaders = ResolveHeaders(args.Headers, args.Values);
bool showHeaders = resolvedHeaders == FieldHeaders.YesAndShow
|| resolvedHeaders == FieldHeaders.NoButGenerate;
bool addFunctionHeaders = args.Functions.Count > 1;
@@ -192,7 +165,7 @@ private InMemoryRange BuildResult(List levels, GroupByBaseArgs args,
if(addFunctionHeaders)
{
- var functionHeaders = ResolveFunctionHeaders(args);
+ var functionHeaders = ResolveFunctionHeaders(args.Functions);
if(args.FunctionLayout == FunctionLayout.Horizontal)
{
for (int c = 0; c < nFunctions; c++)
@@ -247,7 +220,7 @@ private int WriteRows(
int depth)
{
var functionHeaders = args.FunctionLayout == FunctionLayout.Vertical
- ? ResolveFunctionHeaders(args)
+ ? ResolveFunctionHeaders(args.Functions)
: null;
foreach (var level in levels)
@@ -318,7 +291,7 @@ private int CountSubtotalRows(List levels, int subtotalDepth, int de
private int WriteSubtotal(InMemoryRange result, int r, GroupLevel level, int nKeyCols, int nValCols, GroupByBaseArgs args)
{
- var functionHeaders = ResolveFunctionHeaders(args);
+ var functionHeaders = ResolveFunctionHeaders(args.Functions);
if (args.FunctionLayout == FunctionLayout.Vertical)
{
for (int f = 0; f < args.Functions.Count; f++)
@@ -347,7 +320,7 @@ private int WriteSubtotal(InMemoryRange result, int r, GroupLevel level, int nKe
private int WriteGrandTotal(InMemoryRange result, int r, List levels, string label, int nKeyCols, int nValCols, GroupByBaseArgs args, ParsingContext context)
{
- var functionHeaders = ResolveFunctionHeaders(args);
+ var functionHeaders = ResolveFunctionHeaders(args.Functions);
int nAllValCols = args.AllValuesInOrder.Count > 0 ? args.AllValuesInOrder[0].Length : 1;
if (args.FunctionLayout == FunctionLayout.Vertical)
diff --git a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/GroupingFunctions/GroupByFunctionBase.cs b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/GroupingFunctions/GroupByFunctionBase.cs
index 770283a1b..2fdbc7ffc 100644
--- a/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/GroupingFunctions/GroupByFunctionBase.cs
+++ b/src/EPPlus/FormulaParsing/Excel/Functions/RefAndLookup/GroupingFunctions/GroupByFunctionBase.cs
@@ -19,7 +19,6 @@ Date Author Change
using System.Collections.Generic;
using System.Linq;
-
namespace OfficeOpenXml.FormulaParsing.Excel.Functions.RefAndLookup.GroupingFunctions
{
internal abstract class GroupByFunctionBase : ExcelFunction
@@ -29,9 +28,9 @@ internal abstract class GroupByFunctionBase : ExcelFunction
protected const int TotalDepthNoTotals = 0;
protected const int TotalDepthGrandOnly = 1;
- protected List ResolveFunctionHeaders(GroupByBaseArgs args)
+ protected List ResolveFunctionHeaders(List functions)
{
- var names = args.Functions
+ var names = functions
.Select(f => f.EtaFunction != null ? f.EtaFunction.Name : "CUSTOM")
.ToList();
@@ -51,38 +50,30 @@ protected List ResolveFunctionHeaders(GroupByBaseArgs args)
// -------------------------------------------------------
// Argument parsing
// -------------------------------------------------------
- protected bool TryParseBaseArgs(
- IList arguments,
- out GroupByBaseArgs args,
- out CompileResult error)
- {
- args = new GroupByBaseArgs();
- error = null;
-
- if (!arguments[0].IsExcelRange)
- return Fail(eErrorType.Value, out error);
- args.RowFields = arguments[0].ValueAsRangeInfo;
- if (!arguments[1].IsExcelRange)
- return Fail(eErrorType.Value, out error);
- args.Values = arguments[1].ValueAsRangeInfo;
-
- if (args.RowFields.Size.NumberOfRows != args.Values.Size.NumberOfRows)
- return Fail(eErrorType.Value, out error);
+ protected bool Fail(eErrorType err, out CompileResult error)
+ {
+ error = CompileResult.GetErrorResult(err);
+ return false;
+ }
- if (arguments[2].DataType == DataType.LambdaCalculation)
+ protected bool TryParseFunctionArg(FunctionArgument funtionArgument, List functions,
+ out LambdaCalculator function, out FunctionLayout layout)
+ {
+ function = null;
+ layout = FunctionLayout.Single;
+ if (funtionArgument.DataType == DataType.LambdaCalculation)
{
// Single function
- args.Function = arguments[2].Value as LambdaCalculator;
- args.Functions.Add(args.Function);
- args.FunctionLayout = FunctionLayout.Single;
+ function = funtionArgument.Value as LambdaCalculator;
+ functions.Add(function);
}
- else if (arguments[2].IsExcelRange)
+ else if (funtionArgument.IsExcelRange)
{
// Multiple functions via HSTACK or VSTACK
- var range = arguments[2].ValueAsRangeInfo;
+ var range = funtionArgument.ValueAsRangeInfo;
bool isHorizontal = range.Size.NumberOfRows == 1;
- args.FunctionLayout = isHorizontal ? FunctionLayout.Horizontal : FunctionLayout.Vertical;
+ layout = isHorizontal ? FunctionLayout.Horizontal : FunctionLayout.Vertical;
int count = isHorizontal ? range.Size.NumberOfCols : range.Size.NumberOfRows;
for (int i = 0; i < count; i++)
@@ -92,99 +83,61 @@ protected bool TryParseBaseArgs(
: range.GetOffset(i, 0);
if (cellVal is LambdaCalculator lc)
- args.Functions.Add(lc);
+ functions.Add(lc);
else
- return Fail(eErrorType.Value, out error);
+ return false;
}
- args.Function = args.Functions[0];
+ function = functions[0];
}
else
{
- return Fail(eErrorType.Value, out error);
- }
-
- if (args.Functions.Count == 0)
- return Fail(eErrorType.Value, out error);
-
- // field_headers (optional)
- if (arguments.Count > 3 && arguments[3].Value != null)
- {
- var v = Convert.ToInt32(arguments[3].Value);
- if (!Enum.IsDefined(typeof(FieldHeaders), v))
- return Fail(eErrorType.Value, out error);
- args.Headers = (FieldHeaders)v;
- }
- else if(args.Functions.Count > 1) // In excel, if multiple functions are included, headers are by default displayed.
- {
- args.Headers = FieldHeaders.YesAndShow;
+ return false;
}
+ return true;
+ }
- // total_depth (optional)
- if (arguments.Count > 4 && arguments[4].Value != null)
- {
- var totalDepth = Convert.ToInt32(arguments[4].Value);
- if (Math.Abs(totalDepth) > args.RowFields.Size.NumberOfCols)
- return Fail(eErrorType.Value, out error);
- args.TotalDepth = totalDepth;
- }
+ protected bool TryParseTotalDepthArg(FunctionArgument arg, int numberOfCols,
+ out int totalDepth)
+ {
+ totalDepth = Convert.ToInt32(arg.Value);
+ if (Math.Abs(totalDepth) > numberOfCols)
+ return false;
+ return true;
+ }
- // sort_order (optional)
- if (arguments.Count > 5 && arguments[5].Value != null)
+ protected int[] ParseSortOrderArg(FunctionArgument arg)
+ {
+ if (arg.IsExcelRange)
{
- if (arguments[5].IsExcelRange)
- {
- var range = arguments[5].ValueAsRangeInfo;
- bool isHorizontal = range.Size.NumberOfRows == 1;
- int count = isHorizontal ? range.Size.NumberOfCols : range.Size.NumberOfRows;
- args.SortOrders = new int[count];
- for (int i = 0; i < count; i++)
- args.SortOrders[i] = Convert.ToInt32(isHorizontal
- ? range.GetOffset(0, i)
- : range.GetOffset(i, 0));
- }
- else
- {
- args.SortOrders = new[] { Convert.ToInt32(arguments[5].Value) };
- }
+ var range = arg.ValueAsRangeInfo;
+ bool isHorizontal = range.Size.NumberOfRows == 1;
+ int count = isHorizontal ? range.Size.NumberOfCols : range.Size.NumberOfRows;
+ var result = new int[count];
+ for (int i = 0; i < count; i++)
+ result[i] = Convert.ToInt32(isHorizontal
+ ? range.GetOffset(0, i)
+ : range.GetOffset(i, 0));
+ return result;
}
-
- // filter_array (optional)
- if (arguments.Count > 6 && arguments[6].IsExcelRange)
- args.FilterArray = arguments[6].ValueAsRangeInfo;
-
- // field_relationship (optional)
- if (arguments.Count > 7 && arguments[7].Value != null)
+ else
{
- var v = Convert.ToInt32(arguments[7].Value);
- if (!Enum.IsDefined(typeof(FieldRelationship), v))
- return Fail(eErrorType.Value, out error);
- if (v == (int)FieldRelationship.Table && Math.Abs(args.TotalDepth) > 1)
- return Fail(eErrorType.Value, out error);
- args.FieldRelationship = (FieldRelationship)v;
+ return new[] { Convert.ToInt32(arg.Value) };
}
-
- return true;
- }
-
- protected bool Fail(eErrorType err, out CompileResult error)
- {
- error = CompileResult.GetErrorResult(err);
- return false;
}
// -------------------------------------------------------
// Header resolution
// -------------------------------------------------------
- protected FieldHeaders ResolveHeaders(GroupByBaseArgs args)
+ protected FieldHeaders ResolveHeaders(FieldHeaders headers, IRangeInfo values)
{
- if (args.Headers != FieldHeaders.Missing)
- return args.Headers;
+ if (headers != FieldHeaders.Missing)
+ return headers;
- if (args.Values.Size.NumberOfRows < 2)
+ if (values.Size.NumberOfRows < 2)
return FieldHeaders.No;
- var first = args.Values.GetValue(0, 0);
- var second = args.Values.GetValue(1, 0);
+ var first = values.GetValue(0, 0);
+ var second = values.GetValue(1, 0);
bool firstIsText = first is string;
bool secondIsNumber = second is double || second is int || second is long || second is float;
@@ -199,7 +152,7 @@ protected FieldHeaders ResolveHeaders(GroupByBaseArgs args)
// -------------------------------------------------------
protected List BuildGroups(GroupByBaseArgs args, ParsingContext context)
{
- var resolvedHeaders = ResolveHeaders(args);
+ var resolvedHeaders = ResolveHeaders(args.Headers, args.Values);
bool hasHeaders = resolvedHeaders == FieldHeaders.YesAndShow
|| resolvedHeaders == FieldHeaders.YesAndDontShow;
bool multipleFunctions = args.Functions.Count > 1;
@@ -272,6 +225,8 @@ protected List BuildGroups(GroupByBaseArgs args, ParsingContext cont
return levels;
}
+
+
protected List BuildOrderedTree(
Dictionary dict,
List order)
@@ -322,7 +277,7 @@ protected void AggregateTree(List levels, GroupByBaseArgs args, Pars
f.EtaFunction?.Name == "PERCENTOF" ? args.AllValuesInOrder : null);
}
return result;
- }).ToList();
+ }).ToList();
level.SubtotalValue = level.SubtotalValues[0][0];
}
else
@@ -381,5 +336,101 @@ protected object Aggregate(LambdaCalculator calculator, List