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

Introduce equality comparers for OpenXmlElement #1476

Merged
merged 18 commits into from
Jul 9, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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;
using System.Diagnostics.CodeAnalysis;

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;
}

return x.ValueEquality(y, this);
}

/// <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;
}

return obj.GetValueHashCode(this.Options);
}

internal 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 a value indicating whether extended attributes should be considered when determining equality.
/// </summary>
public bool IncludeExtendedAttributes { get; init; } = true;
PhDuck marked this conversation as resolved.
Show resolved Hide resolved

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

/// <summary>
/// Gets a value indicating whether namespace should alone be used when comparing idenity of elements, skipping prefix lookup.
/// </summary>
public bool CompareNamespaceInsteadOfPrefix { get; init; }
PhDuck marked this conversation as resolved.
Show resolved Hide resolved

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

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 +44,47 @@ 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 (this.Ignorable == null == (other.Ignorable == null)) &&
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
(this.Ignorable == null || this.Ignorable.Equals(other.Ignorable)) &&
(this.ProcessContent == null == (other.ProcessContent == null)) &&
(this.ProcessContent == null || this.ProcessContent.Equals(other.ProcessContent)) &&
(this.PreserveElements == null == (other.PreserveElements == null)) &&
(this.PreserveElements == null || this.PreserveElements.Equals(other.PreserveElements)) &&
(this.PreserveAttributes == null == (other.PreserveAttributes == null)) &&
(this.PreserveAttributes == null || this.PreserveAttributes.Equals(other.PreserveAttributes)) &&
(this.MustUnderstand == null == (other.MustUnderstand == null)) &&
(this.MustUnderstand == null || this.MustUnderstand.Equals(other.MustUnderstand));
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
}

/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(
this.Ignorable?.GetHashCode() ?? 0,
this.ProcessContent?.GetHashCode() ?? 0,
this.PreserveElements?.GetHashCode() ?? 0,
this.PreserveAttributes?.GetHashCode() ?? 0,
this.MustUnderstand?.GetHashCode() ?? 0);
}
}
}
202 changes: 202 additions & 0 deletions src/DocumentFormat.OpenXml.Framework/OpenXmlElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,208 @@ internal set

#region public methods

private bool PrefixAndQNameEqual(OpenXmlElement other, OpenXmlElementEqualityOptions options)
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
{
OpenXmlQualifiedName tQName = this.ParsedState.Metadata.QName;
OpenXmlQualifiedName oQName = other.ParsedState.Metadata.QName;

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

if (options.CompareNamespaceInsteadOfPrefix)
{
return true;
}

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

var tPrefix = this.LookupPrefixLocal(ouri);
var oPrefix = other.LookupPrefixLocal(ouri);

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

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

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

/// <summary>
/// Determines value equality of this to <paramref name="other"/> based on the equality as defined by the <paramref name="comparer"/>'s options.
/// </summary>
/// <param name="other">The other OpenXmlElement to equate with.</param>
/// <param name="comparer">The comparer used for this equality determination.</param>
/// <returns></returns>
internal bool ValueEquality(OpenXmlElement? other, OpenXmlElementEqualityComparer comparer)
PhDuck marked this conversation as resolved.
Show resolved Hide resolved
{
if (other == null)
{
return false;
}

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

if (this.GetType() != other.GetType())
{
return false;
}

if (!comparer.Options.RequireParsed)
{
if (!this.XmlParsed && !other.XmlParsed)
{
return string.Equals(this.RawOuterXml, other.RawOuterXml, StringComparison.Ordinal);
}
}

this.MakeSureParsed();
other.MakeSureParsed();

if (this.HasChildren != other.HasChildren)
{
return false;
}

if (!this.PrefixAndQNameEqual(other, comparer.Options))
{
return false;
}

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

if (this.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 = this.ChildElements.GetEnumerator();
OpenXmlElementList.Enumerator oChilds = other.ChildElements.GetEnumerator();

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

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

if (comparer.Options.IncludeExtendedAttributes)
{
if (this.ExtendedAttributesField == null != (other.ExtendedAttributesField == null))
{
return false;
}

if (this.ExtendedAttributesField != null && other.ExtendedAttributesField != null)
{
if (this.ExtendedAttributesField.Count != other.ExtendedAttributesField.Count)
{
return false;
}

for (int i = 0; i < this.ExtendedAttributesField.Count; i++)
{
if (!this.ExtendedAttributesField[i].Equals(other.ExtendedAttributesField[i]))
{
return false;
}
}
}
}

for (int i = 0; i < this.ParsedState.Attributes.Length; i++)
{
var tAttr = this.ParsedState.Attributes[i];
var oAttr = other.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;
}
}

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

return true;
}

/// <summary>
/// Gets a hashcode with values as defined by <paramref name="options"/>.
/// </summary>
/// <param name="options">The options defining what to include.</param>
/// <returns></returns>
internal int GetValueHashCode(OpenXmlElementEqualityOptions options)
{
int hc = options.IncludeMCAttributes && this.MCAttributes != null ? this.MCAttributes.GetHashCode() : 0;

for (int i = 0; i < this.ParsedState.Attributes.Length; i++)
{
if (this.ParsedState.Attributes[i].Value != null)
{
hc = HashCode.Combine(hc, this.ParsedState.Attributes[i].Value?.GetHashCode() ?? 0);
}
}

if (options.IncludeExtendedAttributes)
{
foreach (var attr in this.ExtendedAttributes)
{
hc = HashCode.Combine(hc, attr.GetHashCode());
}
}

if (this.HasChildren)
{
foreach (var child in this.ChildElements)
{
hc = HashCode.Combine(hc, child.GetHashCode());
}
}

return hc;
}

/// <summary>
/// Gets an Open XML attribute with the specified tag name and namespace URI.
/// </summary>
Expand Down
Loading