Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Pivot subtotal styles #1821

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
7 changes: 2 additions & 5 deletions ClosedXML.Tests/Excel/Loading/LoadingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,8 @@ public void CanLoadPivotTableSubtotals()
var ws = wb.Worksheet("PivotTableSubtotals");
var pt = ws.PivotTable("PivotTableSubtotals");

var subtotals = pt.RowLabels.Get("Group").Subtotals.ToArray();
Assert.AreEqual(3, subtotals.Length);
Assert.AreEqual(XLSubtotalFunction.Average, subtotals[0]);
Assert.AreEqual(XLSubtotalFunction.Count, subtotals[1]);
Assert.AreEqual(XLSubtotalFunction.Sum, subtotals[2]);
var subtotals = pt.RowLabels.Get("Group").Subtotals;
CollectionAssert.AreEqual(new[] { XLSubtotalFunction.Automatic, XLSubtotalFunction.Average, XLSubtotalFunction.Count, XLSubtotalFunction.Sum }, subtotals);
}
}

Expand Down
19 changes: 19 additions & 0 deletions ClosedXML.Tests/Excel/PivotTables/XLPivotTableStyleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using ClosedXML.Excel;
using NUnit.Framework;

namespace ClosedXML.Tests.Excel.PivotTables
{
[TestFixture]
public class XLPivotTableStyleTests
{
[Test]
public void PivotSubtotalsStylesLoadingTest()
{
using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\PivotTableReferenceFiles\Styles\subtotals-different-styles-input.xlsx")))
TestHelper.CreateAndCompare(() =>
{
return new XLWorkbook(stream);
}, @"Other\PivotTableReferenceFiles\Styles\subtotals-different-styles-input.xlsx");
}
}
}
31 changes: 30 additions & 1 deletion ClosedXML.Tests/Excel/PivotTables/XLPivotTableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ public void PivotTableStyleFormatsTest()
var namePivotField = pt.RowLabels.Add("Name")
.SetSubtotalCaption("Test caption")
.SetCustomName("Test name")
.AddSubtotal(XLSubtotalFunction.Sum);
.SetSubtotal(XLSubtotalFunction.Sum, true);

ptSheet.SetTabActive();

Expand Down Expand Up @@ -776,6 +776,35 @@ public void ClearPivotTableTenderedTange()
}
}

[Test]
public void CanOmitSubtotalForField()
{
using (var stream = TestHelper.GetStreamFromResource(TestHelper.GetResourcePath(@"Other\PivotTableReferenceFiles\SubtotalsOmitted\inputfile.xlsx")))
TestHelper.CreateAndCompare(() =>
{
var wb = new XLWorkbook(stream);
var pastryTable = wb.Worksheets.Worksheet("PastrySalesData").Tables.Single();
var pvtSheet = wb.Worksheets.Add("pvt");
var pvt = pastryTable.CreatePivotTable(pvtSheet.FirstCell(), "PastryPvt");

pvt.SetSubtotals(XLPivotSubtotals.AtTop);

// Although name is the top field, it has no subtotal, but nested country has.
pvt.RowLabels.Add("Name");
var countryField = pvt.RowLabels.Add("Country")
.SetSubtotalsAtTop(false)
.SetSubtotal(XLSubtotalFunction.Sum, true)
.SetSubtotal(XLSubtotalFunction.Average, true)
.SetSubtotal(XLSubtotalFunction.Count, true);
pvt.RowLabels.Add("Month");
pvt.Values.Add("NumberOfOrders");

pvtSheet.Columns(1, 2).Width = 20;

return wb;
}, @"Other\PivotTableReferenceFiles\SubtotalsOmitted\outputfile.xlsx");
}

private static void SetFieldOptions(IXLPivotField field, bool withDefaults)
{
field.SubtotalsAtTop = !withDefaults;
Expand Down
Binary file modified ClosedXML.Tests/Resource/Examples/PivotTables/PivotTables.xlsx
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6 changes: 3 additions & 3 deletions ClosedXML.Tests/Utils/PackageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,10 +360,10 @@ public static bool Compare(Package left, Package right, bool compareToFirstDiffe
leftPart.ContentType == @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" &&
rightPart.ContentType == @"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml";

var tuple1 = new Tuple<Uri, Stream>(pair.Uri, leftMemoryStream);
var tuple2 = new Tuple<Uri, Stream>(pair.Uri, rightMemoryStream);
var tuple1 = (leftPart.ContentType, Stream: leftMemoryStream);
var tuple2 = (rightPart.ContentType, Stream: rightMemoryStream);

if (!StreamHelper.Compare(tuple1, tuple2, stripColumnWidthsFromSheet))
if (!StreamHelper.Compare(tuple1, tuple2, pair.Uri, stripColumnWidthsFromSheet))
{
pair.Status = CompareStatus.NonEqual;
if (compareToFirstDifference)
Expand Down
159 changes: 54 additions & 105 deletions ClosedXML.Tests/Utils/StreamHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;

namespace ClosedXML.Tests
{
Expand All @@ -11,54 +11,20 @@ namespace ClosedXML.Tests
/// </summary>
public static class StreamHelper
{
/// <summary>
/// Convert stream to byte array
/// </summary>
/// <param name="pStream">Stream</param>
/// <returns>Byte array</returns>
public static byte[] StreamToArray(Stream pStream)
{
long iLength = pStream.Length;
var bytes = new byte[iLength];
for (int i = 0; i < iLength; i++)
{
bytes[i] = (byte)pStream.ReadByte();
}
pStream.Close();
return bytes;
}
private static readonly XName colTagName = XName.Get("col", @"http://schemas.openxmlformats.org/spreadsheetml/2006/main");
private static readonly XName widthAttrName = XName.Get("width");

/// <summary>
/// Convert byte array to stream
/// </summary>
/// <param name="pBynaryArray">Byte array</param>
/// <param name="pStream">Open stream</param>
/// <returns></returns>
public static Stream ArrayToStreamAppend(byte[] pBynaryArray, Stream pStream)
private static readonly IEnumerable<(string PartSubstring, XName NodeName)> ignoredNodes = new List<(string PartSubstring, XName NodeName)>
{
#region Check params

if (ReferenceEquals(pBynaryArray, null))
{
throw new ArgumentNullException("pBynaryArray");
}
if (ReferenceEquals(pStream, null))
{
throw new ArgumentNullException("pStream");
}
if (!pStream.CanWrite)
{
throw new ArgumentException("Can't write to stream", "pStream");
}

#endregion Check params
("/docProps/core.xml", XName.Get("created", @"http://purl.org/dc/terms/")),
("/docProps/core.xml", XName.Get("modified", @"http://purl.org/dc/terms/")),
("sheet", XName.Get("id", @"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"))
};

foreach (byte b in pBynaryArray)
{
pStream.WriteByte(b);
}
return pStream;
}
private static readonly IEnumerable<(string PartSubstring, XName NodeName, XName AttrName)> ignoredAttributes = new List<(string PartSubstring, XName NodeName, XName AttrName)>
{
("sheet", XName.Get("cfRule", @"http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"), XName.Get("id"))
};

public static void StreamToStreamAppend(Stream streamIn, Stream streamToWrite)
{
Expand Down Expand Up @@ -110,91 +76,74 @@ public static void StreamToStreamAppend(Stream streamIn, Stream streamToWrite, l
/// <summary>
/// Compare two streams by converting them to strings and comparing the strings
/// </summary>
/// <param name="one"></param>
/// <param name="other"></param>
/// /// <param name="stripColumnWidths"></param>
/// <returns></returns>
public static bool Compare(Tuple<Uri, Stream> tuple1, Tuple<Uri, Stream> tuple2, bool stripColumnWidths)
public static bool Compare((string ContentType, Stream Stream) left, (string ContentType, Stream Stream) right, Uri partUri, bool stripColumnWidths)
{
#region Check

if (tuple1 == null || tuple1.Item1 == null || tuple1.Item2 == null)
if (left.ContentType == null || left.Stream == null)
{
throw new ArgumentNullException("one");
throw new ArgumentNullException(nameof(left));
}
if (tuple2 == null || tuple2.Item1 == null || tuple2.Item2 == null)
if (right.ContentType == null || right.Stream == null)
{
throw new ArgumentNullException("other");
throw new ArgumentNullException(nameof(right));
}
if (tuple1.Item2.Position != 0)
if (left.Stream.Position != 0)
{
throw new ArgumentException("Must be in position 0", "one");
throw new ArgumentException("Must be in position 0", nameof(left));
}
if (tuple2.Item2.Position != 0)
if (right.Stream.Position != 0)
{
throw new ArgumentException("Must be in position 0", "other");
throw new ArgumentException("Must be in position 0", nameof(right));
}

#endregion Check

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;
}
if (left.ContentType != right.ContentType)
{
throw new ArgumentException("Different content types.");
}

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, "");
#endregion Check

// Collapse empty xml elements
s = emptyXmlElementRegex.Replace(s, "<$1 />");
var leftString = new StreamReader(left.Stream).ReadToEnd();
var rightString = new StreamReader(right.Stream).ReadToEnd();

if (ignoreColumnWidths)
s = RemoveColumnWidths(s);
var isXmlContent = left.ContentType.EndsWith("+xml");
if (!isXmlContent)
return leftString == rightString;

if (ignoreGuids)
s = RemoveGuids(s);
var leftXml = XDocument.Parse(leftString);
RemoveIgnoredParts(leftXml, partUri, stripColumnWidths);
Normalize(leftXml);
var rightXml = XDocument.Parse(rightString);
RemoveIgnoredParts(rightXml, partUri, stripColumnWidths);
Normalize(rightXml);

return s;
return XNode.DeepEquals(leftXml, rightXml);
}

private static IEnumerable<KeyValuePair<string, Regex>> uriSpecificIgnores = new List<KeyValuePair<string, Regex>>()
private static void RemoveIgnoredParts(XDocument document, Uri partUri, Boolean stripColumnWidths)
{
// Remove dcterms elements
new KeyValuePair<string, Regex>("/docProps/core.xml", new Regex(@"<dcterms:(\w+).*?<\/dcterms:\1>", RegexOptions.Compiled))
};
foreach (var ignoredNode in ignoredNodes.Where(i => partUri.OriginalString.Contains(i.PartSubstring)))
document.Descendants(ignoredNode.NodeName).Remove();

private static Regex emptyXmlElementRegex = new Regex(@"<([\w:]+)><\/\1>", RegexOptions.Compiled);
private static Regex columnRegex = new Regex("<x:col.*?width=\"\\d+(\\.\\d+)?\".*?\\/>", RegexOptions.Compiled);
private static Regex widthRegex = new Regex("width=\"\\d+(\\.\\d+)?\"\\s+", RegexOptions.Compiled);
foreach (var ignoredAttr in ignoredAttributes.Where(i => partUri.OriginalString.Contains(i.PartSubstring)))
document.Descendants(ignoredAttr.NodeName).Attributes(ignoredAttr.AttrName).Remove();

private static String RemoveColumnWidths(String s)
{
var replacements = new Dictionary<String, String>();

foreach (var m in columnRegex.Matches(s).OfType<Match>())
{
var original = m.Groups[0].Value;
var replacement = widthRegex.Replace(original, "");
replacements.Add(original, replacement);
}

foreach (var r in replacements)
{
s = s.Replace(r.Key, r.Value);
}
return s;
if (stripColumnWidths)
document.Descendants(colTagName).Attributes(widthAttrName).Remove();
}

private static Regex guidRegex = new Regex(@"{[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}}", RegexOptions.Compiled | RegexOptions.Multiline);

private static String RemoveGuids(String s)
private static void Normalize(XDocument document)
{
return guidRegex.Replace(s, delegate (Match m)
// Turn empty elements into self closing ones.
foreach (var emptyElement in document.Descendants().Where(e => !e.IsEmpty && !e.Nodes().Any()))
emptyElement.RemoveNodes();

foreach (var element in document.Descendants().Where(e => e.Attributes().Any()))
{
return string.Empty;
});
var attrs = element.Attributes().OrderBy(a => a.Name.LocalName).ToList();
element.ReplaceAttributes(attrs);
}
}
}
}
21 changes: 20 additions & 1 deletion ClosedXML/Excel/PivotTables/IXLPivotField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ namespace ClosedXML.Excel
{
public enum XLSubtotalFunction
{
/// <summary>
/// A subtotal function to display a sum for a pivot field. The value is displayed only for a field if
/// no other subtotal is specified.
/// </summary>
Automatic,
None,
Sum,
Expand All @@ -28,7 +32,12 @@ public interface IXLPivotField
String CustomName { get; set; }
String SubtotalCaption { get; set; }

List<XLSubtotalFunction> Subtotals { get; }
/// <summary>
/// Subtotal functions that should be displayed for the field. If empty, no subtotal is displayed for the field.
/// It's not possible to modify the order of displayed subtotal rows in the pivot table.
/// </summary>
IEnumerable<XLSubtotalFunction> Subtotals { get; }

Boolean IncludeNewItemsInFilter { get; set; }

Boolean Outline { get; set; }
Expand All @@ -45,6 +54,16 @@ public interface IXLPivotField

IXLPivotField SetSubtotalCaption(String value);

/// <summary>
/// Change if a subtotal function should be displayed for the pivot field. If the pivot field already contains same subtotal function
/// it won't be added for second time.
/// Function <see cref="XLSubtotalFunction.Automatic"/> is a fallback and won't be displayed, if any other subtotal is set.
/// </summary>
/// <param name="function">A subtotal function to change.</param>
/// <param name="enabled">Should the subtotal function be included in subtotals of the field or not.</param>
IXLPivotField SetSubtotal(XLSubtotalFunction function, bool enabled);

[Obsolete("Use SetSubtotal method instead. This method will be removed in future releases.")]
IXLPivotField AddSubtotal(XLSubtotalFunction value);

IXLPivotField SetIncludeNewItemsInFilter(); IXLPivotField SetIncludeNewItemsInFilter(Boolean value);
Expand Down
11 changes: 11 additions & 0 deletions ClosedXML/Excel/PivotTables/IXLPivotTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,19 @@ public enum XLPivotSortType

public enum XLPivotSubtotals
{
/// <summary>
/// Don't show subtotals for any field in the pivot table.
/// </summary>
DoNotShow,

/// <summary>
/// If a pivot field has one subtotal, it will be displayed at the top of the field. If the field has more than once subtotal, it is displayed at the bottom of the field.
/// </summary>
AtTop,

/// <summary>
/// Display pivot field subtotals at the bottom of the field.
/// </summary>
AtBottom
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace ClosedXML.Excel
{
internal abstract class AbstractPivotFieldReference
{
public Boolean DefaultSubtotal { get; set; }
internal List<XLSubtotalFunction> Subtotals { get; private set; } = new List<XLSubtotalFunction>();

internal abstract UInt32Value GetFieldOffset();

Expand Down
Loading