How can I create a DataField that calculates values in EPPlus?

average, calculated-columns, epplus, epplus-4, excel

Solution

EPPlus does not currently support calculated fields in PivotTables. I used the following workaround:

using System.Linq;
using System.Xml;
using OfficeOpenXml.Table.PivotTable;
using ReflectionMagic;

public static ExcelPivotTableField AddCalculatedField(this ExcelPivotTable pivotTable, string calcFieldName, string formula)
{
    var dynamicPivotTable = pivotTable.AsDynamic();
    var maxIndex = pivotTable.Fields.Max(f => f.Index);
    const string schemaMain = @"http://schemas.openxmlformats.org/spreadsheetml/2006/main";
    var cacheTopNode = dynamicPivotTable.CacheDefinition.CacheDefinitionXml.SelectSingleNode("//d:cacheFields", dynamicPivotTable.NameSpaceManager);
    cacheTopNode.SetAttribute("count", (int.Parse(cacheTopNode.GetAttribute("count")) + 1).ToString());
    var cacheFieldNode = dynamicPivotTable.CacheDefinition.CacheDefinitionXml.CreateElement("cacheField", schemaMain);
    cacheFieldNode.SetAttribute("name", calcFieldName);
    cacheFieldNode.SetAttribute("databaseField", "0");
    cacheFieldNode.SetAttribute("formula", formula);
    cacheFieldNode.SetAttribute("numFmtId", "0");
    cacheTopNode.AppendChild(cacheFieldNode);

    var topNode = dynamicPivotTable.PivotTableXml.SelectSingleNode("//d:pivotFields", dynamicPivotTable.NameSpaceManager);
    topNode.SetAttribute("count", (int.Parse(topNode.GetAttribute("count")) + 1).ToString());
    XmlElement fieldNode = dynamicPivotTable.PivotTableXml.CreateElement("pivotField", schemaMain);
    fieldNode.SetAttribute("compact", "0");
    fieldNode.SetAttribute("outline", "0");
    fieldNode.SetAttribute("showAll", "0");
    fieldNode.SetAttribute("defaultSubtotal", "0");
    topNode.AppendChild(fieldNode);

    var excelPivotTableFieldType = typeof(ExcelPivotTableField).AsDynamicType();
    var excelPivotTableField = excelPivotTableFieldType.New((XmlNamespaceManager)dynamicPivotTable.NameSpaceManager, fieldNode, (ExcelPivotTable)dynamicPivotTable, maxIndex + 1, maxIndex + 1);
    excelPivotTableField.SetCacheFieldNode(cacheFieldNode);
    dynamicPivotTable.Fields.AddInternal(excelPivotTableField);

    return pivotTable.Fields.First(f => f.Name == calcFieldName);
}

Example usage:

var calc1 = pivotTable.AddCalculatedField("Calc1", "BCol*BCol");
var dataField = pivotTable.DataFields.Add(calc1);
dataField.Function = DataFieldFunctions.Sum;
dataField.Name = "Override Name";

Problem

I am porting Excel Interop PivotTable code to EPPlus. I'm at a sticking point in generating a calculated field value. In Excel Interop I can do it this way: ``` pvt.AddDataField(pvt.PivotFields("TotalQty"), "Total Packages", XlConsolidationFunction.xlSum).NumberFormat = "###,##0"; pvt.AddDataField(pvt.PivotFields("TotalPrice"), "Total Purchases", XlConsolidationFunction.xlSum).NumberFormat = "$#,##0.00"; PivotField avg = pvt.CalculatedFields().Add("Average Price", "=TotalPrice/TotalQty", true); avg.Orientation = XlPivotFieldOrientation.xlDataField; avg.NumberFormat = "$###0.00"; ``` With this the "Average Price" value on the PivotTable displays the value for TotalPrice divided by TotalQty. But how to do it in EPPlus? I can create the "plain vanilla" data fields like so: ``` var totQtyField = pivotTable.Fields["TotalQty"]; pivotTable.DataFields.Add(totQtyField); var totPriceField = pivotTable.Fields["TotalPrice"]; pivotTable.DataFields.Add(totPriceField); ``` ...but when it comes to calculating a value, I'm baffled. My starting point was an online example for sums like so: ``` pivotTable.DataFields.Add(pivotTable.Fields["AvgPrice"]).Function = OfficeOpenXml.Table.PivotTable.DataFieldFunctions.Sum(); ``` So I tried the following, but none of them is right: ``` pivotTable.DataFields.Add(pivotTable.Fields["AvgPrice"]).Function = OfficeOpenXml.Table.PivotTable.DataFieldFunctions.Average(totPriceField, totQtyField); pivotTable.DataFields.Add(pivotTable.Fields["AvgPrice"]).Function = "TotalPrice/TotalQty"; pivotTable.DataFields.Add(pivotTable.Fields["AvgPrice"]) = "TotalPrice/TotalQty"; pivotTable.DataFields.Add(pivotTable.Fields["AvgPrice"]) = totPriceField / totQtyField; pivotTable.DataFields.Add(pivotTable.Fields["AvgPrice"]).Function = OfficeOpenXml.Table.PivotTable. DataFieldFunctions.Product(totPriceField/totQtyField); ``` None of those flailings even compile. What adjustment do I need to make, or what completely new approach do I need to take? UPDATE I could calculate the values and put them on the data sheet and reference it that way; but is this really the only/best way to do this? UPDATE 2 I did that (added the vals directly to the sheet that contains the source data for the Pivot Table): ``` var avgCell = rawDataWorksheet.Cells[_lastRowAddedRawData + 1, 9]; if ((TotalPrice < 0.0M) || (Quantity < 1)) { avgCell.Value = 0.0; } else { avgCell.Value = TotalPrice / Quantity; } var prcntgCell = rawDataWorksheet.Cells[_lastRowAddedRawData + 1, 10]; if ((TotalPrice < 0.0M) || (MonthYear == String.Empty)) { prcntgCell.Value = 0.0; } else { prcntgCell.Value = GetPercentageOfItemForMonthYear(TotalPrice, MonthYear); } private double GetPercentageOfItemForMonthYear(decimal totPrice, strin monthYear) { decimal totalForMonthYear = monthlySales[monthYear]; double prcntg = GetPercentageOfItem(totPrice, totalForMonthYear); return prcntg; } private double GetPercentageOfItem(decimal portionOfTotal, decimal grandTotal) { if ((portionOfTotal <= 0.0M) || (grandTotal <= 0.0M)) { return 0.0; } if (portionOfTotal == grandTotal) { return 100.0; } double d = Convert.ToDouble(portionOfTotal) / Convert.ToDouble(grandTotal) * 100; return Math.Round(d, 2); } ``` ...but still would like to know how that's accomplishable using calculated fields while the PivotTable is being created.

Original source