How to check if a list is ordered?
I am doing some unit tests and I want to know if there's any way to test if a list is ordered by a property of the objects it contains.
Right now I am doing it this way but I don't like it, I want a better way. Can somebody help me please?
// (fill the list)
List<StudyFeedI开发者_运维知识库tem> studyFeeds =
Feeds.GetStudyFeeds(2120, DateTime.Today.AddDays(-200), 20);
StudyFeedItem previous = studyFeeds.First();
foreach (StudyFeedItem item in studyFeeds)
{
if (item != previous)
{
Assert.IsTrue(previous.Date > item.Date);
}
previous = item;
}
If you are using MSTest, you may want to take a look at CollectionAssert.AreEqual.
Enumerable.SequenceEqual may be another useful API to use in an assertion.
In both cases you should prepare a list that holds the expected list in the expected order, and then compare that list to the result.
Here's an example:
var studyFeeds = Feeds.GetStudyFeeds(2120, DateTime.Today.AddDays(-200), 20);
var expectedList = studyFeeds.OrderByDescending(x => x.Date);
Assert.IsTrue(expectedList.SequenceEqual(studyFeeds));
A .NET 4.0 way would be to use the Enumerable.Zip
method to zip the list with itself offset by one, which pairs each item with the subsequent item in the list. You can then check that the condition holds true for each pair, e.g.
var ordered = studyFeeds.Zip(studyFeeds.Skip(1), (a, b) => new { a, b })
.All(p => p.a.Date < p.b.Date);
If you're on an earlier version of the framework you can write your own Zip method without too much trouble, something like the following (argument validation and disposal of the enumerators if applicable is left to the reader):
public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
this IEnumerable<TFirst> first,
IEnumerable<TSecond> second,
Func<TFirst, TSecond, TResult> selector)
{
var e1 = first.GetEnumerator();
var e2 = second.GetEnumerator();
while (e1.MoveNext() & e2.MoveNext()) // one & is important
yield return selector(e1.Current, e2.Current);
}
Nunit 2.5 introduced CollectionOrderedContraint and a nice syntax for verifying the order of a collection:
Assert.That(collection, Is.Ordered.By("PropertyName"));
No need to manually order and compare.
If your unit testing framework has helper methods to assert equality of collections, you should be able do something like this (NUnit flavored):
var sorted = studyFeeds.OrderBy(s => s.Date);
CollectionAssert.AreEqual(sorted.ToList(), studyFeeds.ToList());
The assert method works with any IEnumerable
, but when both collections are of type IList
or "array of something", the error message thrown when the assert fails will contain the index of the first out-of-place element.
The solutions posted involving sorting the list are expensive - determining if a list IS sorted can be done in O(N). Here's an extension method which will check:
public static bool IsOrdered<T>(this IList<T> list, IComparer<T> comparer = null)
{
if (comparer == null)
{
comparer = Comparer<T>.Default;
}
if (list.Count > 1)
{
for (int i = 1; i < list.Count; i++)
{
if (comparer.Compare(list[i - 1], list[i]) > 0)
{
return false;
}
}
}
return true;
}
A corresponding IsOrderedDescending
could be implemented easily by changing > 0
to < 0
.
Greg Beech answer, although excellent, can be simplified further by performing the test in the Zip itself. So instead of:
var ordered = studyFeeds.Zip(studyFeeds.Skip(1), (a, b) => new { a, b })
.All(p => p.a.Date <= p.b.Date);
You can simply do:
var ordered = !studyFeeds.Zip(studyFeeds.Skip(1), (a, b) => a.Date <= b.Date)
.Contains(false);
Which saves you one lambda expression and one anonymous type.
(In my opinion removing the anonymous type also makes it easier to read.)
if(studyFeeds.Length < 2)
return;
for(int i = 1; i < studyFeeds.Length;i++)
Assert.IsTrue(studyFeeds[i-1].Date > studyFeeds[i].Date);
for
isn't dead just quite yet!
How about:
var list = items.ToList();
for(int i = 1; i < list.Count; i++) {
Assert.IsTrue(yourComparer.Compare(list[i - 1], list[i]) <= 0);
}
where yourComparer
is an instance of YourComparer
which implements IComparer<YourBusinessObject>
. This ensures that every element is less than the next element in the enumeration.
Linq based answer is:
You can use SequenceEqual
method to check if the original and ordered one is same or not.
var isOrderedAscending = lJobsList.SequenceEqual(lJobsList.OrderBy(x => x));
var isOrderedDescending = lJobsList.SequenceEqual(lJobsList.OrderByDescending(x => x));
Don't forget to import System.Linq
namespace.
Additionally:
I am repeating that this answer is Linq based, you can achieve more efficiency by creating your custom extension method.
Also, if somebody still wants to use Linq and check if the sequence both is ordered in ascending or descending order, then you can achieve a little bit more efficiency like that:
var orderedSequence = lJobsList.OrderBy(x => x)
.ToList();
var reversedOrderSequence = orderedSequence.AsEnumerable()
.Reverse();
if (lJobsList.SequenceEqual(orderedSequence))
{
// Ordered in ascending
}
else (lJobsList.SequenceEqual(reversedOrderSequence))
{
// Ordered in descending
}
You could use an extension method like this:
public static System.ComponentModel.ListSortDirection? SortDirection<T>(this IEnumerable<T> items, Comparer<T> comparer = null)
{
if (items == null) throw new ArgumentNullException("items");
if (comparer == null) comparer = Comparer<T>.Default;
bool ascendingOrder = true; bool descendingOrder = true;
using (var e = items.GetEnumerator())
{
if (e.MoveNext())
{
T last = e.Current; // first item
while (e.MoveNext())
{
int diff = comparer.Compare(last, e.Current);
if (diff > 0)
ascendingOrder = false;
else if (diff < 0)
descendingOrder = false;
if (!ascendingOrder && !descendingOrder)
break;
last = e.Current;
}
}
}
if (ascendingOrder)
return System.ComponentModel.ListSortDirection.Ascending;
else if (descendingOrder)
return System.ComponentModel.ListSortDirection.Descending;
else
return null;
}
It enables to check if the sequence is sorted and also determines the direction:
var items = new[] { 3, 2, 1, 1, 0 };
var sort = items.SortDirection();
Console.WriteLine("Is sorted? {0}, Direction: {1}", sort.HasValue, sort);
//Is sorted? True, Direction: Descending
Here's how I do it with Linq and I comparable, might not be the best but works for me and it's test framework independent.
So the call looks like this:
myList.IsOrderedBy(a => a.StartDate)
This works for anything that implements IComparable, so numbers strings and anything that inherit from IComparable:
public static bool IsOrderedBy<T, TProperty>(this List<T> list, Expression<Func<T, TProperty>> propertyExpression) where TProperty : IComparable<TProperty>
{
var member = (MemberExpression) propertyExpression.Body;
var propertyInfo = (PropertyInfo) member.Member;
IComparable<TProperty> previousValue = null;
for (int i = 0; i < list.Count(); i++)
{
var currentValue = (TProperty)propertyInfo.GetValue(list[i], null);
if (previousValue == null)
{
previousValue = currentValue;
continue;
}
if(previousValue.CompareTo(currentValue) > 0) return false;
previousValue = currentValue;
}
return true;
}
Hope this helps, took me ages to work this one out.
Checking a sequence can have four different outcomes. Same
means that all elements in the sequence are the same (or the sequence is empty):
enum Sort {
Unsorted,
Same,
SortedAscending,
SortedDescending
}
Here is a way to check the sorting of a sequence:
Sort GetSort<T>(IEnumerable<T> source, IComparer<T> comparer = null) {
if (source == null)
throw new ArgumentNullException(nameof(source));
if (comparer == null)
comparer = Comparer<T>.Default;
using (var enumerator = source.GetEnumerator()) {
if (!enumerator.MoveNext())
return Sort.Same;
Sort? result = null;
var previousItem = enumerator.Current;
while (enumerator.MoveNext()) {
var nextItem = enumerator.Current;
var comparison = comparer.Compare(previousItem, nextItem);
if (comparison < 0) {
if (result == Sort.SortedDescending)
return Sort.Unsorted;
result = Sort.SortedAscending;
}
else if (comparison > 0) {
if (result == Sort.SortedAscending)
return Sort.Unsorted;
result = Sort.SortedDescending;
}
}
return result ?? Sort.Same;
}
}
I'm using the enumerator directly instead of a foreach
loop because I need to examine the elements of the sequence as pairs. It makes the code more complex but is also more efficient.
Something LINQ-y would be to use a separate sorted query...
var sorted = from item in items
orderby item.Priority
select item;
Assert.IsTrue(items.SequenceEquals(sorted));
Type inference means you'd need a
where T : IHasPriority
However, if you have multiple items of the same priority, then for a unit test assertion you're probably best off just looping with the list index as Jason suggested.
One way or another you're going to have to walk the list and ensure that the items are in the order you want. Since the item comparison is custom, you could look into creating a generic method for this and passing in a comparison function - the same way that sorting the list uses comparison functions.
You can create an ordered and an unordered version of the list first:
var asc = jobs.OrderBy(x => x);
var desc = jobs.OrderByDescending(x => x);
Now compare the original list with both:
if (jobs.SequenceEqual(asc) || jobs.SequenceEquals(desc)) // ...
var studyFeeds = Feeds.GetStudyFeeds(2120, DateTime.Today.AddDays(-200), 20);
var orderedFeeds = studyFeeds.OrderBy(f => f.Date);
for (int i = 0; i < studyFeeds.Count; i++)
{
Assert.AreEqual(orderedFeeds[i].Date, studyFeeds[i].Date);
}
What about something like this, without sorting the list
public static bool IsAscendingOrder<T>(this IEnumerable<T> seq) where T : IComparable
{
var seqArray = seq as T[] ?? seq.ToArray();
return !seqArray.Where((e, i) =>
i < seqArray.Count() - 1 &&
e.CompareTo(seqArray.ElementAt(i + 1)) >= 0).Any();
}
Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert.AreEqual(
mylist.OrderBy((a) => a.SomeProperty).ToList(),
mylist,
"Not sorted.");
Here's a more lightweight generic version. To test for descending order, change the >= 0 comparison to <= 0.
public static bool IsAscendingOrder<T>(this IEnumerable<T> seq) where T : IComparable<T>
{
var predecessor = default(T);
var hasPredecessor = false;
foreach(var x in seq)
{
if (hasPredecessor && predecessor.CompareTo(x) >= 0) return false;
predecessor = x;
hasPredecessor = true;
}
return true;
}
Tests:
- new int[] { }.IsAscendingOrder() returns true
- new int[] { 1 }.IsAscendingOrder() returns true
- new int[] { 1,2 }.IsAscendingOrder() returns true
- new int[] { 1,2,0 }.IsAscendingOrder() returns false
While AnorZaken's and Greg Beech's answers are very nice, as they don't require using an extension method, it can be good to avoid Zip() sometimes, as some enumerables can be expensive to enumerate in this way.
A solution can be found in Aggregate()
double[] score1 = new double[] { 12.2, 13.3, 5, 17.2, 2.2, 4.5 };
double[] score2 = new double[] { 2.2, 4.5, 5, 12.2, 13.3, 17.2 };
bool isordered1 = score1.Aggregate(double.MinValue,(accum,elem)=>elem>=accum?elem:double.MaxValue) < double.MaxValue;
bool isordered2 = score2.Aggregate(double.MinValue,(accum,elem)=>elem>=accum?elem:double.MaxValue) < double.MaxValue;
Console.WriteLine ("isordered1 {0}",isordered1);
Console.WriteLine ("isordered2 {0}",isordered2);
One thing a little ugly about the above solution, is the double less-than comparisons. Floating comparisons like this make me queasy as it is almost like a floating point equality comparison. But it seems to work for double here. Integer values would be fine, also. The floating point comparison can be avoided by using nullable types, but then the code becomes a bit harder to read.
double[] score3 = new double[] { 12.2, 13.3, 5, 17.2, 2.2, 4.5 };
double[] score4 = new double[] { 2.2, 4.5, 5, 12.2, 13.3, 17.2 };
bool isordered3 = score3.Aggregate((double?)double.MinValue,(accum,elem)=>(elem>(accum??(double?)double.MaxValue).Value)?(double?)elem:(double?)null) !=null;
bool isordered4 = score4.Aggregate((double?)double.MinValue,(accum,elem)=>(elem>(accum??(double?)double.MaxValue).Value)?(double?)elem:(double?)null) !=null;
Console.WriteLine ("isordered3 {0}",isordered3);
Console.WriteLine ("isordered4 {0}",isordered4);
You can use lambda in extension:
public static bool IsAscending<T>(this IEnumerable<T> self, Func<T, T, int> compareTo) {
var list = self as IList<T> ?? self.ToList();
if (list.Count < 2) {
return true;
}
T a = list[0];
for (int i = 1; i < list.Count; i++) {
T b = list[i];
if (compareTo(a, b) > 0) {
return false;
}
a = b;
}
return true;
}
Using:
bool result1 = Enumerable.Range(2, 10).IsAscending((a, b) => a.CompareTo(b));
more:
var lst = new List<(int, string)> { (1, "b"), (2, "a"), (3, "s1"), (3, "s") };
bool result2 = lst.IsAscending((a, b) => {
var cmp = a.Item1.CompareTo(b.Item1);
if (cmp != 0) {
return cmp;
} else {
return a.Item2.CompareTo(b.Item2);
}
});
var expectedList = resultA.ToArray();
var actualList = resultB.ToArray();
var i = 0;
foreach (var item in expectedList)
{
Assert.True(expectedList[i].id == actualList[i].id);
i++;
}
精彩评论