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

Make simple types comparable and equatable #487

Merged
merged 10 commits into from
Apr 24, 2018

Conversation

ThomasBarnekow
Copy link
Collaborator

@ThomasBarnekow ThomasBarnekow commented Apr 20, 2018

In conjunction with #485, this commit introduces two base classes,
OpenXmlComparableReferenceType and OpenXmlComparableValueType, which
implement the IComparable, IComparable<T>, and IEquatable<T>
interfaces. With the exception of EnumValue and ListValue, all children
or descendants of OpenXmlSimpleType are changed to become subclasses of
one of those two base classes.

This commit further adds unit tests for the new behavior. The core
tests are contained in the OpenXmlComparableReferenceTypeTests and
OpenXmlComparableValueTypeTests base classes. For each concrete simple
type, subclasses of those base classes create the concrete values to be
used in the generic unit tests.

Lastly, this commit fixes existing unit tests within the SimpleTypeTest
and OpenXmlSimpleValueTest classes, because those unit tests asserted
that two instances with same value but constructed separately are NOT
equal.

Note that the implementation is not yet complete as it lacks comments
for some public methods, for example. We might also have to add further
unit tests.

In conjunction with dotnet#485, this commit introduces two base classes,
OpenXmlComparableReferenceType and OpenXmlComparableValueType, which
implement the IComparable, IComparable<T>, and IEquatable<T>
interfaces. With the exception of EnumValue and ListValue, all children
or descendants of OpenXmlSimpleType are changed to become subclasses of
one of those two base classes.

This commit further adds unit tests for the new behavior. The core
tests are contained in the OpenXmlComparableReferenceTypeTests and
OpenXmlComparableValueTypeTests base classes. For each concrete simple
type, subclasses of those base classes create the concrete values to be
used in the generic unit tests.

Lastly, this commit fixes existing unit tests within the SimpleTypeTest
and OpenXmlSimpleValueTest classes, because those unit tests asserted
that two instances with same value but constructed separately are NOT
equal.

Note that the implementation is not yet complete as it lacks comments
for some public methods, for example. We might also have to add further
unit tests.
This commit removes the IConvertible type constraint on the
OpenXmlComparableValueType class. While working just fine in a local
VS2017 build, this constraint led to build errors on the build server.
This commit fixes the DecimalValueTest, DoubleValueTest, and
SingleValueTest methods of the OpenXmlSimpleValueTest class. Those
methods failed on machines using a locale (or culture settings) that
are not compatible with the en-US locale (or invariant culture).

Fixes dotnet#488.
@ThomasBarnekow
Copy link
Collaborator Author

@twsouthwick I've seen that the build failed because what are mere warnings in a local build is taken as an error on the build server. For example, the warning (or error on the build server) is that certain classes should implement Equals because they are implementing IComparable. However, they don't need to implement Equals, because all they would do is delegate that to the base class, which renders that implementation unnecessary.

I did not want to disable warnings etc., because I wanted to discuss this whole approach with you anyhow. But note that it builds just fine on my machine and all unit tests pass.

@@ -10,15 +10,14 @@ namespace DocumentFormat.OpenXml
/// Represents the enum value for attributes.
/// </summary>
/// <typeparam name="T">Every enum value must be an enum with the EnumStringValueAttribute object.</typeparam>
[DebuggerDisplay("{InnerText}")]
[DebuggerDisplay("{" + nameof(InnerText) + "}")]
public class EnumValue<T> : OpenXmlSimpleValue<T>
where T : struct
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For v3.0, we can add a constraint for Enum (as of C# 7.3) :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@twsouthwick Do you want me to do anything here? ReSharper suggested this change and I've used nameof in my code as much as possible to avoid issues as names change (which might not be the case here).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I saw that change as I quickly glanced through. I like the use of nameof as much as possible and see the value here. However, that name won't be changing, so I'd probably not make the change myself as it takes a bit more time to grok. I'll leave that up to you, though

/// <summary>
/// Creates a new instance of <see cref="OpenXmlComparableReferenceType{T}"/>.
/// </summary>
protected OpenXmlComparableReferenceType()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make these constructors private protected? I'd like to limit visibility of new types/methods as much as possible unless we have a reason to make them public

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, can do.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the existing constructors of OpenXmlSimpleType and OpenXmlSimpleValue<T>, which are all protected? Should we consider making those private protected in v3.0?

@twsouthwick
Copy link
Member

Thanks for the submission! I'll take a look in the next couple days. As far as the warnings go, sounds like that may be an issue with the StyleCop.Analyzers. You can disable the warnings for each class with an explanation. We should file an issue at their repo for that because it does seem unnecessary

@twsouthwick
Copy link
Member

My comment was wrong - this isn't StyleCop errors, the error id is CA1036 which is for Code Analysis

@twsouthwick
Copy link
Member

I've created an issue on their repo that you can reference when you disable the code with a pragma: dotnet/roslyn-analyzers#1671

This commit disables CA1036 for subclasses of the generic base classes
OpenXmlComparableReferenceType and OpenXmlComparableValueType to avoid
the unnecessary warning that those subclasses should implement Equals.
See dotnet/roslyn-analyzers#1671 for details.

This commit also adds file headers where those were missing.
This commit adds missing file headers and moves the DoubleValueTests
class into its own file.
/// <summary>
/// Gets or sets the value of this instance.
/// </summary>
public abstract T Value { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be abstract? Or can it just be virtual (or even non-virtual)? It would nice to follow the pattern of OpenXmlSimpleValue<T>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that as well. Currently, only three subclasses inherit from OpenXmlComparableReferenceType<T>:

  • Base64BinaryValue,
  • HexBinaryValue, and
  • StringValue.

All of those basically delegate to OpenXmlSimpleType.TextValue. Therefore, I thought about two options:

  1. turn OpenXmlComparableReferenceType<T> into something like OpenXmlStringType and provide a concrete implementation of the Value property, or
  2. leave OpenXmlComparableReferenceType<T> generic to enable reuse for non-string types.

For the second option, which I chose for this implementation, I thought it was more elegant to just keep the that property abstract. However, we could also go for Option 1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah - I hadn't realized it was a generic type. I like the current implementation better so it's more reusable.

return CompareTo(other);
}

throw new ArgumentException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a message here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easy answer: yes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha now the hard question is figuring out what to put :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my current thinking (and I am very much open for suggestions):

        public int CompareTo(object obj)
        {
            switch (obj)
            {
                case null:
                    return 1;

                case OpenXmlComparableReferenceType<T> other:
                    return CompareTo(other);
            }

            throw new ArgumentException("Incompatible type: " + obj.GetType().FullName);
        }

I also used a switch statement with pattern matching.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with that - just stick it in the resource file

return other.Value == null;
}

return Value.Equals(other.Value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be collapsed into Equals(Value, other?.Value)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That depends on whether a non-null instance with a null Value property should be equal to a null instance.

For example, I could have two non-null StringValue instances, with Value being null for both of them. Those two should be considered equal. However, when I take any of those, I'd say that stringValue.Equals(null) should be false. In this case, the shortened Equals method could look like this:

        public bool Equals(OpenXmlComparableReferenceType<T> other)
        {
            return !(other is null) && Equals(Value, other.Value);
        }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I still think that Equals(Value, other?.Value) would work, but I guess I'm not sure off the top of my head what Equals(null,null) is. Do you have unit tests covering the cases you bring up? If so you can just try it out. I'll leave this up to you - no need to rathole into it as the current implementation works

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Equals(null, null) should be true.

/// Represents a comparable and equatable value for simple value types (Int32, UInt32, Byte, struct, etc).
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
public abstract class OpenXmlComparableValueType<T> : OpenXmlSimpleValue<T>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if OpenXmlComparableValueType<T> : OpenXmlComparableReferenceType<T?>? This could probably consolidate a bit of code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that's probably a breaking change (it would change the inheritance chain of some of the current values)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also thought about doing more cleanup. However, I've tried to limit the changes to the inheritance chain to the minimum to not introduce a breaking change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's good for now. We can get the functionality in, and then mark it for clean up with an issue opened against the v3.0 milestone

Copy link
Member

@twsouthwick twsouthwick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a couple of comments about naming. Other than this, it all looks good. We can just work through any comments and then get it merged in

/// Represents a comparable and equatable reference.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
public abstract class OpenXmlComparableReferenceType<T> : OpenXmlSimpleType,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name feels a little weird. Can we follow the previous pattern and go with OpenXmlComparableSimpleType?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is a little weird, so I am open for better names. But let me explain the thinking behind the current name.

On the master branch, we have OpenXmlSimpleType and OpenXmlSimpleValue<T> where the latter derives from the former. The subclasses representing value types (e.g., DecimalValue) and EnumValue<T> derive from OpenXmlSimpleValue<T>. The string-valued (reference) types (e.g., StringValue, HexBinaryValue) and ListValue<T> derive from OpenXmlSimpleType.

Now, to not introduce a breaking change, I needed to introduce:

  1. one subclass of OpenXmlSimpleValue<T> (and therefore a grandchild of OpenXmlSimpleType) for all those comparable and equatable value types, hence the name OpenXmlComparableValueType<T>; and
  2. one subclass of OpenXmlSimpleType that would become the superclass of the "reference types" StringValue, HexBinaryValue, and Base64BinaryValue.

For the second, the best name I could come up with was OpenXmlComparableReferenceType, because that was in contrast to OpenXmlComparableValueType, which was kind of given by its parent class OpenXmlSimpleValue.

Now, since both of those new, abstract base classes are "comparable simple types", we should not call just one of them OpenXmlComparableSimpleType.

Having said that, I am totally not wedded to the current names. If we find something that is logical, I am fine with that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, naming is hard 😆 I see your thought process and I think the issue I'm having with the name is "ValueType"/"ReferenceType" doesn't imply a kind of OpenXmlSimpleType. I want to make sure it's easy to see the relationship in the names (if possible). How about OpenXmlComparableSimpleValue<T> and OpenXmlComparableSimpleReference<T>. Not sure if they should be suffixed with "Type", but I think what I'm trying to get is a name that is obviously a subclass of OpenXmlSimpleType.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenXmlComparableSimpleValue<T> and OpenXmlComparableSimpleReference<T> would be fine for me. I think I thought about the SimpleReference as well, but wasn't sure whether that introduces an unwanted connotation. But if you are fine with those, we can certainly also use those two.

And if you like OpenXmlComparableSimpleType<T> better, we could also go with that. As noted in my other comment, my only thought would be whether OpenXmlComparableSimpleValue<T> should derive from OpenXmlComparableSimpleType<T>, if possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the first two. I agree they're not ideal, but it's a good compromise to keep naming consistent.

If we can get OpenXmlComparableSimpleValue<T> to derive from OpenXmlComparableSimpleType<T> that would be awesome, but I think it would break the hierarchy. Note the PR has a package-compat-analyzer check I wrote to validate that we don't break binary compatibility, so if you're not sure, you can submit an update and see what that analyzer says.

/// Represents a comparable and equatable value for simple value types (Int32, UInt32, Byte, struct, etc).
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
public abstract class OpenXmlComparableValueType<T> : OpenXmlSimpleValue<T>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the previous comment for the reference type, I think OpenXmlComparableSimpleValue would fit better with the current naming

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking your two comments together:

  1. OpenXmlComparableReferenceType<T> becomes OpenXmlComparableSimpleType<T>
  2. OpenXmlComparableValueType<T> becomes OpenXmlComparableSimpleValue<T>

I am totally fine with (2). I am just thinking that you'd expect OpenXmlComparableSimpleValue<T> to derive from OpenXmlComparableSimpleType<T> just as OpenXmlSimpleValue<T> derives from OpenXmlSimpleType.

Can we do that without breaking anything?

The constraints on T are different for both classes, because one wants T to be a class while the other wants T to be a struct. Not sure what happened if we left out the class constraint and only added struct in the child (for the value types).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we were to derive such as OpenXmlComparableSimpleValue<T> : OpenXmlComparableSimpleType<T?> would that work? I still think we'd end up breaking the inheritance chain which we'd need to do for v3.0. If you agree, can you open an issue to track making this change in v3.0?

This commit introduces enhancements as discussed on GitHub and adds
further unit tests.
@ThomasBarnekow
Copy link
Collaborator Author

@twsouthwick I've pushed a commit with the changes we've discussed.

BTW, I am getting messages that Travis CI builds are failing.

@twsouthwick
Copy link
Member

What Travis CI failures are you referring to? I only know of AppVeyor

Copy link
Member

@twsouthwick twsouthwick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! The last thing to fix is to remove the comments I pointed out in the tests discussing what will break and what won't. After this change, that will become the current behavior

@@ -84,7 +85,13 @@ public void BooleanValueTest()
Log.Comment("Verifying reference type behavior...");
objA = new BooleanValue(validValue);
objB = new BooleanValue(validValue);
Log.VerifyFalse(object.Equals(objA, objB), "Two instances with same value but constructed separated are Equal.");

// This is the previous assertion, which fails due to the proposed change:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove these comments and update it to use Assert.Equal instead - the Log file system is something I'd like to move away from as XUnit provides most of the testing stuff (and helpful diagnostics) we need

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove the comments that I added. However, those Log.VerifyFalse() calls are from the original author of those unit tests and there are a lot of those ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I'm not saying remove all the Log.Verify[...] calls, just switch to XUnit for any new code :) I've been chipping away at the removal of the old Log infrastructure for a while now... I definitely don't expect that in this PR :)

@ThomasBarnekow
Copy link
Collaborator Author

The Travis CI failures came from an outdated build that I had once configured for my repo when we originally set up build servers for the Open XML SDK.

/// <param name="right"></param>
/// <returns></returns>
public static bool operator <(OpenXmlComparableSimpleReference<T> left, OpenXmlComparableSimpleReference<T> right)
{
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the comments on those operators (==, !=, <, <=, >, >=)? I have not completed them but don't know whether we really need them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm for completeness, either fill them in (probably some boiler plate comments), or just remove them (unless if stylecop complains - I can't remember if I've turned those on yet)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added those empty comments because Style Cop did complain. I will steal something elsewhere.

@twsouthwick
Copy link
Member

Ah, yes, if you set up TravisCI for an old build system, it will most definitely break. The new project uses new csproj and requires VS 2017 Update 3 (or 5... can't remember). Right now we're using AppVeyor, but it would be nice to expand that out to test on Mono and multiple locales (and potentially Xamarin and .NET Native), but that would be for a different (or set of) PRs

ThomasBarnekow and others added 3 commits April 23, 2018 16:52
This commit removes comments added to existing unit tests to describe
the change in behavior. Further, it replaces Log.VerifyTrue() with
Assert.True() for the parts of the unit tests that establish equality.
This commit adds missing comments for the operators ==, !=, <, <=, >,
and >=.
Copy link
Member

@twsouthwick twsouthwick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Once the build finishes I'll merge it

@twsouthwick twsouthwick merged commit 3cc2657 into dotnet:master Apr 24, 2018
@twsouthwick
Copy link
Member

The test that failed is one I just added... I'll merge this and then figure out what's causing those failures

@ThomasBarnekow ThomasBarnekow deleted the feature/simple-type-equality branch September 7, 2019 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants