Equality constraints and IEnumerable confusion

70 views
Skip to first unread message

Berryl Hesh

unread,
Sep 22, 2010, 7:41:53 PM9/22/10
to NUnit-Discuss
Below is code for an abstract generic range class based on code
written by Jon Skeet in his C# In Depth book, a DateTime
implementation of same, and a test that at first glance I would expect
to pass using NUnit equals constraints (latest version). As you can
see, a major part of the class design is to provide support for
enumeration, although it is obviously not a collection class.

Is there some way to use the equals constraint in this situation?

Cheers,
Berryl

public abstract class Range<T> : ValueObject, IEnumerable<T> where
T : IComparable<T>, IEquatable<T>
{
private readonly T _end;
private readonly T _start;

/// <summary> The start of the range. </summary>
public T Start { get { return _start; } }

/// <summary> The end of the range. </summary>
public T End { get { return _end; } }

protected Range(T start, T end)
{
if (start.CompareTo(end) > 0)
throw new ArgumentOutOfRangeException();
_start = start;
_end = end;
}

#region IEnumerable<T> Members

public IEnumerator<T> GetEnumerator()
{
var value = _start;
while (value.CompareTo(_end) < 0)
{
yield return value;
value = GetNextValue(value);
}
if (value.CompareTo(_end) == 0)
yield return value;
}

IEnumerator IEnumerable.GetEnumerator() { return
GetEnumerator(); }

#endregion

/// <summary> True if the first range includes the passed
point.</summary>
public bool Contains(T value)
{
return value.CompareTo(_start) >= 0 &&
value.CompareTo(_end) <= 0;
}

protected abstract T GetNextValue(T current);

#region Equality

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return ReferenceEquals(this, obj) || Equals(obj as
Range<T>);
}

public bool Equals(Range<T> other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return other.Start.Equals(Start) && other.End.Equals(End);
}

public override int GetHashCode()
{
unchecked
{
var result = 17;
result = (result * 397) ^ Start.GetHashCode();
result = (result * 397) ^ End.GetHashCode();
return result;
}
}

public static bool operator ==(Range<T> left, Range<T> right)
{ return Equals(left, right); }
public static bool operator !=(Range<T> left, Range<T> right)
{ return !Equals(left, right); }

#endregion

}

public class DateTimeRange : Range<DateTime>
{
readonly TimeSpan _step;

public DateTimeRange(DateTime start, DateTime end)
: this(start, end, TimeSpan.FromDays(1)) { }

public DateTimeRange(DateTime start, DateTime end, TimeSpan
step)
: base(start, end) { this._step = step; }

protected override DateTime GetNextValue(DateTime current)
{ return current + _step; }

}

[Test]
public void DateTime_EndOfRange_EffectsEquality()
{
var start = DateTime.Now;
var end = start.AddDays(1);
var r1 = new DateTimeRange(start, end);
var r2 = new DateTimeRange(start, end);
Assert.That(r1, Is.EqualTo(r2));
Assert.That(r1.GetHashCode(),
Is.EqualTo(r2.GetHashCode()));
Assert.That(r1 == r2);
Assert.That(r1.Equals(r2));

r2 = new DateTimeRange(start, end.AddTicks(1));

//Assert.That(r1,
Is.Not.EqualTo(r2));==============================> FALSE <==
Assert.That(r1.GetHashCode(),
Is.Not.EqualTo(r2.GetHashCode()));

Assert.That(r1 != r2);
Assert.That(!r1.Equals(r2));
}
public T Start { get { return _start; } }

public T End { get { return _end; } }

#region IEnumerable<T> Members

public IEnumerator<T> GetEnumerator() {
var value = _start;
while (value.CompareTo(_end) < 0) {
yield return value;
value = GetNextValue(value);
}
if (value.CompareTo(_end) == 0)
yield return value;
}

IEnumerator IEnumerable.GetEnumerator() { return
GetEnumerator(); }

#endregion

public bool Contains(T value) {
return value.CompareTo(_start) >= 0 &&
value.CompareTo(_end) <= 0;
}

protected abstract T GetNextValue(T current);
}

Berryl Hesh

unread,
Sep 23, 2010, 11:05:42 AM9/23/10
to NUnit-Discuss
Ok, too much information in that last post for a simple question.

Given a class that implements IEnumerable, is there a preferred way in
v2.5 to force equality to be determined by the Equals method in that
class (without trying to enumerate it)? Will that change in v3.x if
not?

Cheers,
Berryl

Charlie Poole

unread,
Sep 23, 2010, 8:40:38 PM9/23/10
to nunit-...@googlegroups.com
Hi Berryl,

Indeed - I wasn't able to figure out the problem until you gave that hint. :-)

I've always recognized that the NUnit special way of determining equality
of enumerables might theoretically cause a problem but this is the first time
I've seen an actual case where the problem arose.

You can force your Equals method to be used by using one of the
variations of the Using modifier...

Assert.That(r1, Is.Not.EqualTo(r2).Using(comparer));

where comparer implements one of the following:
* IComparer
* IComparer<T>
* Comparison<T>
* IEqualityComparer
* IEqualityComparer<T>

This is a satisfactory workaround but I'm not happy with it as a real
solution. It seems like we should default to the Equals override IFF
it's a true override and not just the object.Equals method.

What do you think?

Charlie

> --
> You received this message because you are subscribed to the Google Groups "NUnit-Discuss" group.
> To post to this group, send email to nunit-...@googlegroups.com.
> To unsubscribe from this group, send email to nunit-discus...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/nunit-discuss?hl=en.
>
>

Berryl Hesh

unread,
Sep 23, 2010, 11:14:39 PM9/23/10
to NUnit-Discuss
I'd regrettably rather just do a boolean Assert.That(r1 != r2) than
write a comparer *just* to use the equality constraint. The last bit
sounds like the right solution to the problem. Intuitive, effective,
elegant, even. I am out of adjectives to motivate you further :--)

Cheers,
Berryl

Charlie Poole

unread,
Sep 24, 2010, 8:57:22 AM9/24/10
to nunit-...@googlegroups.com
Added a bug for this...
http://bugs.launchpad.net/nunit-3.0/+bug/646786

Charlie

cliff vaughn

unread,
Sep 24, 2010, 9:36:55 AM9/24/10
to nunit-discuss
couldn't you try to explicitly cast your object to object to avoid the
collection overrides?

--
thanks

cliff

Charlie Poole

unread,
Sep 24, 2010, 9:44:09 AM9/24/10
to nunit-...@googlegroups.com
NUnit will examine the object, see that it implements IEnumerable and
use its default equal comparison for IEnumerables. In 99% of cases,
this is convenient.

Charlie

Reply all
Reply to author
Forward
0 new messages