-
Notifications
You must be signed in to change notification settings - Fork 551
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
Make simple types comparable and equatable #487
Conversation
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.
@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 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 |
There was a problem hiding this comment.
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) :)
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, can do.
There was a problem hiding this comment.
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?
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 |
My comment was wrong - this isn't StyleCop errors, the error id is |
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; } |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
, andStringValue
.
All of those basically delegate to OpenXmlSimpleType.TextValue
. Therefore, I thought about two options:
- turn
OpenXmlComparableReferenceType<T>
into something likeOpenXmlStringType
and provide a concrete implementation of theValue
property, or - 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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Easy answer: yes.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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)
?
There was a problem hiding this comment.
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);
}
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this 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, |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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:
- one subclass of
OpenXmlSimpleValue<T>
(and therefore a grandchild ofOpenXmlSimpleType
) for all those comparable and equatable value types, hence the nameOpenXmlComparableValueType<T>
; and - one subclass of
OpenXmlSimpleType
that would become the superclass of the "reference types"StringValue
,HexBinaryValue
, andBase64BinaryValue
.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
OpenXmlComparableReferenceType<T>
becomesOpenXmlComparableSimpleType<T>
OpenXmlComparableValueType<T>
becomesOpenXmlComparableSimpleValue<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).
There was a problem hiding this comment.
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.
@twsouthwick I've pushed a commit with the changes we've discussed. BTW, I am getting messages that Travis CI builds are failing. |
What Travis CI failures are you referring to? I only know of AppVeyor |
There was a problem hiding this 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: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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 :)
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) | ||
{ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
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 |
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 >=.
There was a problem hiding this 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
The test that failed is one I just added... I'll merge this and then figure out what's causing those failures |
In conjunction with #485, this commit introduces two base classes,
OpenXmlComparableReferenceType
andOpenXmlComparableValueType
, whichimplement the
IComparable
,IComparable<T>
, andIEquatable<T>
interfaces. With the exception of
EnumValue
andListValue
, all childrenor descendants of
OpenXmlSimpleType
are changed to become subclasses ofone of those two base classes.
This commit further adds unit tests for the new behavior. The core
tests are contained in the
OpenXmlComparableReferenceTypeTests
andOpenXmlComparableValueTypeTests
base classes. For each concrete simpletype, 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 assertedthat 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.