Skip to content

Commit

Permalink
Introduce equality comparers for OpenXmlElement (#1476)
Browse files Browse the repository at this point in the history
  • Loading branch information
PhDuck authored Jul 9, 2023
1 parent e7a0331 commit 31289b6
Show file tree
Hide file tree
Showing 6 changed files with 786 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;

namespace DocumentFormat.OpenXml
{
/// <summary>
/// Equality comparer for determining value equality for <see cref="OpenXmlElement"/>.
/// </summary>
public static class OpenXmlElementComparers
{
/// <summary>
/// Gets the default equality comparer.
/// </summary>
public static IEqualityComparer<OpenXmlElement> Default { get; } = Create(new OpenXmlElementEqualityOptions());

/// <summary>
/// Creates a <see cref="IEqualityComparer{OpenXmlElement}"/> based on the given options./>
/// </summary>
/// <param name="openXmlElementEqualityOptions">The options defining equality.</param>
/// <returns></returns>
public static IEqualityComparer<OpenXmlElement> Create(OpenXmlElementEqualityOptions openXmlElementEqualityOptions)
{
return new OpenXmlElementEqualityComparer(openXmlElementEqualityOptions);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using DocumentFormat.OpenXml.Framework;

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

namespace DocumentFormat.OpenXml
{
internal sealed class OpenXmlElementEqualityComparer : IEqualityComparer<OpenXmlElement>
{
/// <summary>
/// Gets the options regulating how equality is defined.
/// </summary>
internal OpenXmlElementEqualityOptions Options { get; }

internal OpenXmlElementEqualityComparer(OpenXmlElementEqualityOptions options)
{
this.Options = options;
}

/// <summary>
/// Determines equality for two given <see cref="OpenXmlElement"/>.
/// </summary>
/// <param name="x">First object.</param>
/// <param name="y">Second object.</param>
/// <returns></returns>
public bool Equals(OpenXmlElement? x, OpenXmlElement? y)
{
if (ReferenceEquals(x, y))
{
return true;
}

if (x == null || y == null)
{
return false;
}

if (x.GetType() != y.GetType())
{
return false;
}

if (!this.CheckAndEquateSpecialOptions(x, y))
{
return false;
}

if (x.HasChildren != y.HasChildren)
{
return false;
}

if (!OpenXmlElementEqualityComparer.PrefixAndQNameEqual(x, y, this.Options))
{
return false;
}

if (x is OpenXmlLeafTextElement)
{
if (!string.Equals(x.InnerText, y.InnerText, StringComparison.Ordinal))
{
return false;
}
}

if (x.HasChildren)
{
// DEVNOTE: Do not refactor this to use "simpler" for construct.
// The indexer on ChildElement walks the linked list for each operation,
// not maintaining state so it turns into a O(n^2) operation.
OpenXmlElementList.Enumerator tChilds = x.ChildElements.GetEnumerator();
OpenXmlElementList.Enumerator oChilds = y.ChildElements.GetEnumerator();

int e1 = 0, e2 = 0;
while (OpenXmlElementEqualityComparer.MoveNextAndTrackCount(ref tChilds, ref oChilds, ref e1, ref e2))
{
if (!this.Equals(tChilds.Current, oChilds.Current))
{
return false;
}
}

// Different amount of children.
if (e1 != e2)
{
return false;
}
}

for (int i = 0; i < x.ParsedState.Attributes.Length; i++)
{
var tAttr = x.ParsedState.Attributes[i];
var oAttr = y.ParsedState.Attributes[i];

if ((tAttr.Value == null && oAttr.Value != null) || (tAttr.Value != null && oAttr.Value == null))
{
return false;
}

if (tAttr.Value == null)
{
continue;
}

if (!tAttr.Value.Equals(oAttr.Value))
{
return false;
}
}

return true;
}

/// <summary>
/// Handles checking of all options that changes the behaviour of equality based on options in <see cref="OpenXmlElementEqualityOptions"/>.
/// </summary>
private bool CheckAndEquateSpecialOptions(OpenXmlElement x, OpenXmlElement y)
{
if (!this.Options.RequireParsed)
{
if (!x.XmlParsed && !y.XmlParsed)
{
return string.Equals(x.RawOuterXml, y.RawOuterXml, StringComparison.Ordinal);
}
}

x.MakeSureParsed();
y.MakeSureParsed();

if (this.Options.IncludeExtendedAttributes)
{
if (x.ExtendedAttributes == null != (y.ExtendedAttributes == null))
{
return false;
}

if (x.ExtendedAttributes != null && y.ExtendedAttributes != null)
{
if (x.ExtendedAttributes.Count() != y.ExtendedAttributes.Count())
{
return false;
}

for (int i = 0; i < x.ExtendedAttributes.Count(); i++)
{
if (!x.ExtendedAttributes.ElementAt(i).Equals(y.ExtendedAttributes.ElementAt(i)))
{
return false;
}
}
}
}

if (this.Options.IncludeMCAttributes)
{
if (x.MCAttributes == null != (y.MCAttributes == null) || (x.MCAttributes != null && !x.MCAttributes.Equals(y.MCAttributes)))
{
return false;
}
}

return true;
}

/// <summary>
/// Calculates a hashcode based on the given <see cref="OpenXmlElement"/> object.
/// </summary>
/// <param name="obj">The object to get a hashcode for.</param>
/// <returns></returns>
public int GetHashCode([DisallowNull] OpenXmlElement obj)
{
if (obj == null)
{
return 0;
}

var hc = default(HashCode);

if (this.Options.IncludeMCAttributes)
{
hc.Add(obj.MCAttributes);
}

for (int i = 0; i < obj.ParsedState.Attributes.Length; i++)
{
if (obj.ParsedState.Attributes[i].Value != null)
{
hc.Add(obj.ParsedState.Attributes[i].Value);
}
}

if (this.Options.IncludeExtendedAttributes)
{
foreach (OpenXmlAttribute attr in obj.ExtendedAttributes)
{
hc.Add(attr);
}
}

if (obj.HasChildren)
{
foreach (OpenXmlElement child in obj.ChildElements)
{
hc.Add(child);
}
}

return hc.ToHashCode();
}

private static bool PrefixAndQNameEqual(OpenXmlElement x, OpenXmlElement y, OpenXmlElementEqualityOptions options)
{
OpenXmlQualifiedName tQName = x.ParsedState.Metadata.QName;
OpenXmlQualifiedName oQName = y.ParsedState.Metadata.QName;

if (!tQName.Equals(oQName))
{
return false;
}

if (options.SkipPrefixComparison)
{
return true;
}

string turi = tQName.Namespace.Uri;
string ouri = oQName.Namespace.Uri;

var tPrefix = x.LookupPrefixLocal(ouri);
var oPrefix = y.LookupPrefixLocal(ouri);

if (string.IsNullOrEmpty(tPrefix))
{
tPrefix = x.Features.GetNamespaceResolver().LookupPrefix(turi);
}

if (string.IsNullOrEmpty(oPrefix))
{
oPrefix = y.Features.GetNamespaceResolver().LookupPrefix(ouri);
}

return string.Equals(tPrefix, oPrefix, StringComparison.Ordinal);
}

private static bool MoveNextAndTrackCount(ref OpenXmlElementList.Enumerator e1, ref OpenXmlElementList.Enumerator e2, ref int e1ctr, ref int e2ctr)
{
if (e1.MoveNext())
{
e1ctr++;
if (e2.MoveNext())
{
e2ctr++;
return true;
}
}
else if (e2.MoveNext())
{
e2ctr++;
}

return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace DocumentFormat.OpenXml
{
/// <summary>
/// Options defining the behaviour of equality for <see cref="OpenXmlElement"/>.
/// </summary>
public sealed class OpenXmlElementEqualityOptions
{
/// <summary>
/// Gets or sets a value indicating whether extended attributes should be considered when determining equality.
/// </summary>
public bool IncludeExtendedAttributes { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether mC attributes should be considered when determining equality.
/// </summary>
public bool IncludeMCAttributes { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether namespace should alone be used when comparing idenity of elements, skipping prefix lookup to improve performance.
/// </summary>
public bool SkipPrefixComparison { get; set; }

/// <summary>
/// Gets or sets a value indicating whether elements must be parsed which ensures order of schema is used instead of input ordering.
/// </summary>
public bool RequireParsed { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using DocumentFormat.OpenXml.Framework;
using System;

namespace DocumentFormat.OpenXml
{
/// <summary>
/// Defines the Markup Compatibility Attributes.
/// </summary>
public class MarkupCompatibilityAttributes
public class MarkupCompatibilityAttributes : IEquatable<MarkupCompatibilityAttributes>
{
/// <summary>
/// Gets or sets a whitespace-delimited list of prefixes, where each
Expand Down Expand Up @@ -42,5 +45,45 @@ public class MarkupCompatibilityAttributes
/// a set of namespace names.
/// </summary>
public StringValue? MustUnderstand { get; set; }

/// <inheritdoc/>
public override bool Equals(object? obj)
{
return this.Equals(obj as MarkupCompatibilityAttributes);
}

/// <inheritdoc/>
public bool Equals(MarkupCompatibilityAttributes? other)
{
if (other == null)
{
return false;
}

if (object.ReferenceEquals(this, other))
{
return true;
}

return Equals(this.Ignorable, other.Ignorable)
&& Equals(this.ProcessContent, other.ProcessContent)
&& Equals(this.PreserveElements, other.PreserveElements)
&& Equals(this.PreserveAttributes, other.PreserveAttributes)
&& Equals(this.MustUnderstand, other.MustUnderstand);
}

/// <inheritdoc/>
public override int GetHashCode()
{
var hc = default(HashCode);

hc.Add(this.Ignorable);
hc.Add(this.ProcessContent);
hc.Add(this.PreserveElements);
hc.Add(this.PreserveAttributes);
hc.Add(this.MustUnderstand);

return hc.ToHashCode();
}
}
}
Loading

0 comments on commit 31289b6

Please sign in to comment.