C# Ranking of objects, multiple criteria - c#

I am building a plugin for a LAN party website that I wrote that would allow the use of a Round Robin tournament.
All is going well, but I have some questions about the most efficient way to rank over two criteria.
Basically, I would like the following ranking layout:
Rank Wins TotalScore
PersonE 1 5 50
PersonD 2 3.5 37
PersonA 2 3.5 37
PersonC 4 2.5 26
PersonB 5 2.5 24
PersonF 6 0 12
In SQL server, I would use:
SELECT
[Person],
RANK() OVER (ORDER BY Wins DESC, TotalScore DESC) [Rank],
[Wins],
[TotalScore]
Now, I only have List, Dictionary, and etc. to work with
Specifically:
Dictionary<TournamentTeam, double> wins = new Dictionary<TournamentTeam, double>();
Dictionary<TournamentTeam, double> score = new Dictionary<TournamentTeam, double>();
Is there a way to do this style of ranking with LINQ?
If not, is there an extensible way that would allow me later to take in to account Win-Loss-Draw instead of just wins if I choose to?
Edit:
My adaptation of TheSoftwareJedi's answer:
private class RRWinRecord : IComparable
{
public int Wins { get; set; }
public int Losses { get; set; }
public int Draws { get; set; }
public double OverallScore { get; set; }
public double WinRecord
{
get
{
return this.Wins * 1.0 + this.Draws * 0.5 + this.Losses * 0.0;
}
}
public int CompareTo(object obj) { ... }
public override bool Equals(object obj) { ... }
public override int GetHashCode() { ... }
public static bool operator ==(RRWinRecord lhs, RRWinRecord rhs) { ... }
public static bool operator !=(RRWinRecord lhs, RRWinRecord rhs) { ... }
public static bool operator >(RRWinRecord lhs, RRWinRecord rhs) { ... }
public static bool operator <(RRWinRecord lhs, RRWinRecord rhs) { ... }
public static bool operator >=(RRWinRecord lhs, RRWinRecord rhs) { ... }
public static bool operator <=(RRWinRecord lhs, RRWinRecord rhs) { ... }
}
...
int r = 1, lastRank = 1;
RRWinRecord lastRecord = null;
var ranks = from team in records.Keys
let teamRecord = records[team]
orderby teamRecord descending
select new RRRank() { Team = team, Rank = r++, Record = teamRecord };
foreach (var rank in ranks)
{
if (rank.Record != null && lastRecord == rank.Record)
{
rank.Rank = lastRank;
}
lastRecord = rank.Record;
lastRank = rank.Rank;
string scoreDescription = String.Format("{0}-{1}-{2}", rank.Record.Wins, rank.Record.Losses, rank.Record.Draws);
yield return new TournamentRanking(rank.Team, rank.Rank, scoreDescription);
}
yield break;

Ranking isn't too hard. Just mishmash OrderBy and Select implementation patterns together and you can have an easy to use Ranking extension method. Like this:
public static IEnumerable<U> Rank<T, TKey, U>
(
this IEnumerable<T> source,
Func<T, TKey> keySelector,
Func<T, int, U> selector
)
{
if (!source.Any())
{
yield break;
}
int itemCount = 0;
T[] ordered = source.OrderBy(keySelector).ToArray();
TKey previous = keySelector(ordered[0]);
int rank = 1;
foreach (T t in ordered)
{
itemCount += 1;
TKey current = keySelector(t);
if (!current.Equals(previous))
{
rank = itemCount;
}
yield return selector(t, rank);
previous = current;
}
}
Here's some test code
string[] myNames = new string[]
{ "Bob", "Mark", "John", "Jim", "Lisa", "Dave" };
//
var query = myNames.Rank(s => s.Length, (s, r) => new { s, r });
//
foreach (var x in query)
{
Console.WriteLine("{0} {1}", x.r, x.s);
}
Which yields these results:
1 Bob
1 Jim
3 Mark
3 John
3 Lisa
3 Dave

Assuming you have a List<Result> structure where the Result object has the following parameters...
Pesron - string
Rank - int
Wins - double
TotalScore - int
You could write a custom comparer, and then pass that to List.Sort(Comparison<Result> comparison)
Alternative, you could just make your Result object implement IComparable<Result>
and stick this in your class.
#region IComparable Members
public int CompareTo(Result obj)
{
if (this.Rank.CompareTo(obj.Rank) != 0)
return this.Rank.CompareTo(obj.Rank);
if (this.Wins.CompareTo(obj.Wins) != 0)
return (this.Wins.CompareTo(obj.Wins);
return (this.TotalScore.CompareTo(obj.TotalScore) ;
}
#endregion
Then you can just call List<Result>.Sort();

This should work for a non-dense rank:
static class Program
{
static IEnumerable<Result> GetResults(Dictionary<TournamentTeam, double> wins, Dictionary<TournamentTeam, double> scores)
{
int r = 1;
double lastWin = -1;
double lastScore = -1;
int lastRank = 1;
foreach (var rank in from name in wins.Keys
let score = scores[name]
let win = wins[name]
orderby win descending, score descending
select new Result { Name = name, Rank = r++, Score = score, Win = win })
{
if (lastWin == rank.Win && lastScore == rank.Score)
{
rank.Rank = lastRank;
}
lastWin = rank.Win;
lastScore = rank.Score;
lastRank = rank.Rank;
yield return rank;
}
}
}
class Result
{
public TournamentTeam Name;
public int Rank;
public double Score;
public double Win;
}

This could be a start:
Dictionary<TournamentTeam, double> wins = new Dictionary<TournamentTeam, double>();
Dictionary<TournamentTeam, double> score = new Dictionary<TournamentTeam, double>();
Dictionary<TournamentTeam, int> ranks = new Dictionary<TournamentTeam, int>();
int r = 1;
ranks = (
from name
in wins.Keys
orderby wins[name] descending, scores[name] descending
select new { Name = name, Rank = r++ })
.ToDictionary(item => item.Name, item => item.Rank);

I realize I'm late to the party, but I wanted to take a shot anyhow.
Here is a version which uses LINQ exclusively:
private IEnumerable<TeamRank> GetRankings(Dictionary<TournamentTeam, double> wins, Dictionary<TournamentTeam, double> scores)
{
var overallRank = 1;
return
from team in wins.Keys
group team by new { Wins = wins[team], TotalScore = scores[team] } into rankGroup
orderby rankGroup.Key.Wins descending, rankGroup.Key.TotalScore descending
let currentRank = overallRank++
from team in rankGroup
select new TeamRank(team, currentRank, rankGroup.Key.Wins, rankGroup.Key.TotalScore);
}
The return type:
public class TeamRank
{
public TeamRank(TournamentTeam team, int rank, double wins, double totalScore)
{
this.Team = team;
this.Rank = rank;
this.Wins = wins;
this.TotalScore = totalScore;
}
public TournamentTeam Team { get; private set; }
public int Rank { get; private set; }
public double Wins { get; private set; }
public double TotalScore { get; private set; }
}

Related

C# all common sequences in lists of strings

I am trying to find the longest common sequence of strings within the provided arrays.
I have 25,000 lists with sequences, with a total of 450,000 of words that I need to order by length, then by count.
List<string> listA = new List<string>() {"Step1", "Step3", "Process", "System", "Process"};
List<string> listB = new List<string>() {"Process", "System", "Process"};
List<string> listC = new List<string>() {"Terminal", "Step1", "Step3"};
...
The desired output that prints all possible sequences and their length and count is:
Sequence Length Count
Step1->Step3->Process->System->Process 5 1
Step1->Step3->Process->System 4 1
Step3->Process->System->Process 4 1
Process->System->Process 3 2
Step1->Step3->Process 3 1
Step3->Process->System 3 1
Terminal->Step1->Step3 3 1
Step1->Step3 2 2
Process->System 2 2
System->Process 2 2
Step3->Process 2 1
Terminal->Step1 2 1
Process 1 4
Step1 1 2
Step3 1 2
System 1 2
Terminal 1 1
I could only find an implementation of substrings, and not whole words that can take multiple lists as input.
Ok so you can actually overload GetHashCode and Equals to treat strings like chars in a string. Also it might be reasonable create list segment to prevent flooding runtime with multiple collections.
public class ListSegment<T>
{
private sealed class ListSegmentEqualityComparer : IEqualityComparer<ListSegment<T>>
{
public bool Equals(ListSegment<T> x, ListSegment<T> y)
{
if (x.Length != y.Length)
{
return false;
}
return x.Lst.Skip(x.Offset).Take(x.Length)
.SequenceEqual(y.Lst.Skip(y.Offset).Take(y.Length));
}
public int GetHashCode(ListSegment<T> obj)
{
unchecked
{
int hash = 17;
for (int i = obj.Offset; i < obj.Offset + obj.Length; i++)
{
hash = hash * 31 + obj.Lst[i].GetHashCode();
}
return hash;
}
}
}
public static IEqualityComparer<ListSegment<T>> Default { get; } = new ListSegmentEqualityComparer();
public List<T> Lst { get; set; }
public int Offset { get; set; }
public int Length { get; set; }
public IEnumerable<T> GetEnumerable()
{
return Lst.Skip(Offset).Take(Length);
}
public override string ToString()
{
return string.Join("->", GetEnumerable());
}
}
And then you run through list of lists counting number of occurrences
public List<KeyValuePair<ListSegment<string>, int>> GetOrderedPairs(List<List<string>> data)
{
var segmentsDictionary = new Dictionary<ListSegment<string>, int>(ListSegment<string>.Default);
foreach (var list in data)
{
for (int i = 0; i < list.Count; i++)
for (int j = i + 1; j <= list.Count; j++)
{
var segment = new ListSegment<string>
{
Lst = list,
Length = j-i,
Offset = i,
};
if (segmentsDictionary.TryGetValue(segment, out var val))
{
segmentsDictionary[segment] = val + 1;
}
else
{
segmentsDictionary[segment] = 1;
}
}
}
return segmentsDictionary.OrderByDescending(pair => pair.Key.Length).ToList();
}
To test it run following
List<string> listA = new List<string>() { "Step1", "Step3", "Process", "System", "Process" };
List<string> listB = new List<string>() { "Process", "System", "Process" };
List<string> listC = new List<string>() { "Terminal", "Step1", "Step3" };
var pairs = GetOrderedPairs(new List<List<string>>()
{
listA, listB, listC
});
foreach (var keyValuePair in pairs)
{
Console.WriteLine(keyValuePair.Key + " " + keyValuePair.Key.Length + " " + keyValuePair.Value);
}
Using some extension methods, you can create an IEQualityComparer that compares IEnumerable sequences. Using this, you can use LINQ Distinct to compare by sequences:
public static class IEnumerableExt {
public static IEnumerable<IEnumerable<T>> DistinctIE<T>(this IEnumerable<IEnumerable<T>> src) => src.Distinct(Make.IESequenceEqualityComparer<T>());
// IEnumerable<string>
public static string Join(this IEnumerable<string> src, string sep) => String.Join(sep, src);
}
public static class Make {
public static IEqualityComparer<IEnumerable<T>> IESequenceEqualityComparer<T>() => new IEnumerableSequenceEqualityComparer<T>();
public static IEqualityComparer<IEnumerable<T>> IESequenceEqualityComparer<T>(T _) => new IEnumerableSequenceEqualityComparer<T>();
public class IEnumerableSequenceEqualityComparer<T> : IEqualityComparer<IEnumerable<T>> {
public bool Equals(IEnumerable<T> x, IEnumerable<T> y) =>
Object.ReferenceEquals(x, y) || (x != null && y != null && (x.SequenceEqual(y)));
public int GetHashCode(IEnumerable<T> src) {
var hc = new HashCode();
foreach (var v in src)
hc.Add(v);
return hc.ToHashCode();
}
}
}
With these tools, you can create an extension method to generate all the subsequences of a List and all the distinct subsequences:
public static class ListExt {
public static IEnumerable<IEnumerable<T>> Subsequences<T>(this List<T> src) {
IEnumerable<T> Helper(int start, int end) {
for (int j3 = start; j3 <= end; ++j3)
yield return src[j3];
}
for (int j1 = 0; j1 < src.Count; ++j1) {
for (int j2 = j1; j2 < src.Count; ++j2)
yield return Helper(j1, j2);
}
}
public static IEnumerable<IEnumerable<T>> DistinctSubsequences<T>(this List<T> src) => src.Subsequences().DistinctIE();
}
Now you can compute the answer.
First, compute all the subsequences and combine them:
var ssA = listA.DistinctSubsequences();
var ssB = listB.DistinctSubsequences();
var ssC = listC.DistinctSubsequences();
var ssAll = ssA.Concat(ssB).Concat(ssC).DistinctIE();
Then, create some helpers for counting occurrences:
var hA = ssA.ToHashSet(Make.IESequenceEqualityComparer<string>());
var hB = ssB.ToHashSet(Make.IESequenceEqualityComparer<string>());
var hC = ssC.ToHashSet(Make.IESequenceEqualityComparer<string>());
Func<IEnumerable<string>, HashSet<IEnumerable<string>>, int> testIn = (s, h) => h.Contains(s) ? 1 : 0;
Func<IEnumerable<string>,int> countIn = s => testIn(s,hA)+testIn(s,hB)+testIn(s,hC);
Finally, compute the answer:
var ans = ssAll.Select(ss => new { Sequence = ss.Join("->"), Length = ss.Count(), Count = countIn(ss) }).OrderByDescending(sc => sc.Sequence.Length);

Linq generic GroupBy date and other fields at the same time

I would like to be able to group some database results by year/month/week/day as well as other properties at the same time using Linq. The user should be able to switch between grouping by year/month/week/day at runtime. The typical situation is that i have some kind of DTO containing a DateTime timestamp as well as other properties.
UPDATE: I found a working solution. See post below.
For grouping only by date (excluding grouping by other properties), I have created a DateGroupKey class and an extension method:
DateGroupKey class
public class DateGroupKey
{
public int Year { get; set; }
public int Month { get; set; }
public int Week { get; set; }
public int Day { get; set; }
}
Extension method
public static IEnumerable<IGrouping<DateGroupKey, TValue>> GroupByDate<TValue>(
this IEnumerable<TValue> source,
Func<TValue, DateTime> dateSelector,
DateGroupType dateGroupType)
{
Func<TValue, DateGroupKey> keySelector = null;
switch (dateGroupType)
{
case DateGroupType.Year:
keySelector = val => new DateGroupKey { Year = dateSelector(val).Year };
break;
case DateGroupType.Month:
keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month };
break;
case DateGroupType.Week:
keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Week = dateSelector(val).GetIso8601WeekOfYear() };
break;
case DateGroupType.Day:
keySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month, Day = dateSelector(val).Day };
break;
default:
throw new NotSupportedException($"Group type not supported: {dateGroupType}");
}
return source.GroupBy(keySelector, new DateGroupKeyComparer());
}
where the DateGroupKeyComparer is a class implementing IEqualityComparer<DateGroupKey>. I can use the extension method like this:
var grouped = results.GroupByDate<ProductDto>(x => x.TimeStamp, DateGroupType.Month)
which works as it should. Now, I would like to expand on this idea and be able to group not only by date but also some of the other fields at the same time. Say the ProductDtoclass has the following signature
public class ProductDto
{
public DateTime TimeStamp {get; set;}
public int ProductGroup {get; set;}
public int Variant {get; set;}
public double Value {get; set;}
}
I would like to do something along the lines of
var grouped = results.GroupByDate<ProductDto>(x => x.TimeStamp, x => x.ProductGroup, x => x.Variant, DateGroupType.Month);
but I can't seem to wrap my head around how to aproach this. I thought about abstracting the DateGroupKey into an IDateGroupKey interface holding the Year, Month, Week and Day properties, and somehow tell the GroupBy extension how to map to this specific implementation, but I am not sure whether it is the right (or simplay 'a') way to go.
Any good ideas of how to aproach this?
I would say that you need to do something like this:
my model class
public class MyClass
{
public MyClass(DateTime date, string name, int partNumber)
{
Date = date;
Name = name;
PartNumber = partNumber;
}
public DateTime Date { get;}
public string Name { get; }
public int PartNumber {get;}
}
extension method to get week number:
public static class DateTimeExtensions
{
public static int WeekNumber(this DateTime date)
{
var cal = new GregorianCalendar();
var week = cal.GetWeekOfYear(date, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
return week;
}
}
and then grouping.
var dataBase = new[]{
new MyClass(new DateTime(2018,1,1),"Jane",10),
new MyClass(new DateTime(2017,1,1),"Jane",1),
new MyClass(new DateTime(2016,1,1),"Jane",10),
new MyClass(new DateTime(2018,2,1),"Jane",1),
new MyClass(new DateTime(2018,2,2),"Jane",10),
new MyClass(new DateTime(2017,2,2),"Jane",1),
new MyClass(new DateTime(2016,2,2),"Jane",10),
new MyClass(new DateTime(2018,2,8),"Jane",1),
new MyClass(new DateTime(2017,2,8),"Jane",10),
new MyClass(new DateTime(2016,2,8),"Jane",1),
new MyClass(new DateTime(2018,3,1),"Jane",10),
new MyClass(new DateTime(2017,3,1),"Jane",1),
new MyClass(new DateTime(2016,3,1),"Jane",10),
};
var myCollection = new MyClasses(dataBase);
while (true)
{
Console.WriteLine("Group By What");
var type = Console.ReadLine();
var enumType = (MyClasses.GroupType) Enum.Parse(typeof(MyClasses.GroupType), $"GroupBy{type.UppercaseFirst()}");
foreach (var g in myCollection.GetGroupedValue(enumType))
{
Console.WriteLine(g.Key);
}
}
}
You can create your own collection that will do what you would like. It could look something like this:
public class MyClasses : Collection<MyClass>
{
public enum GroupType
{
GroupByYear = 0,
GroupByMonth = 1,
GroupByWeek = 2
}
public MyClasses(IEnumerable<MyClass> classes)
{
foreach (var myClass in classes)
{
Add(myClass);
}
}
public IEnumerable<IGrouping<object, MyClass>> GroupByYear => this.GroupBy(x => x.Date.Year);
public IEnumerable<IGrouping<object, MyClass>> GroupByMonth => this.GroupBy(x => new { x.Date.Year, x.Date.Month});
public IEnumerable<IGrouping<object, MyClass>> GroupByWeek => this.GroupBy(x => new { x.Date.Year, x.Date.WeekNumber()});
public IEnumerable<IGrouping<object, MyClass>> GetGroupedValue(GroupType groupType)
{
var propertyName = groupType.ToString();
var property = GetType().GetProperty(propertyName);
return property?.GetValue(this) as IEnumerable<IGrouping<object, MyClass>>;
}
}
this will group by the date requirements that you are asking for.
Update:
So, I found a working solution - I am not quite satisfied but it will do for now.
I have created a generic composite key, holding a DateGroupKey object containing information on the date-grouping as well as a generic object key (to identity grouping fields not part of the date)
public class DateGroupCompositeKey<TKey>
{
public DateGroupKey DateKey { get; set; }
public TKey ObjectKey { get; set; }
}
public class DateGroupKey
{
public int Year { get; set; }
public int Month { get; set; }
public int Week { get; set; }
public int Day { get; set; }
}
Also, I implemented IEqualityComparer for these classes.
public class DateGroupKeyComparer : IEqualityComparer<DateGroupKey>
{
public bool Equals(DateGroupKey x, DateGroupKey y)
{
return x.Year == y.Year &&
x.Month == y.Month &&
x.Week == y.Week &&
x.Day == y.Day;
}
public int GetHashCode(DateGroupKey obj)
{
unchecked
{
var h = 0;
h = (h * 31) ^ obj.Year;
h = (h * 31) ^ obj.Month;
h = (h * 31) ^ obj.Week;
h = (h * 31) ^ obj.Day;
return h;
}
}
}
public class DateGroupCompositeKeyComparer<TValue> : IEqualityComparer<DateGroupCompositeKey<TValue>>
{
private readonly IEqualityComparer<TValue> _valueComparer;
private readonly DateGroupKeyComparer _dgkComparer;
public DateGroupCompositeKeyComparer(IEqualityComparer<TValue> valueComparer)
{
_valueComparer = valueComparer;
_dgkComparer = new DateGroupKeyComparer();
}
public bool Equals(DateGroupCompositeKey<TValue> x, DateGroupCompositeKey<TValue> y)
{
return _dgkComparer.Equals(x.DateKey, y.DateKey) &&
_valueComparer.Equals(x.ObjectKey, y.ObjectKey);
}
public int GetHashCode(DateGroupCompositeKey<TValue> obj)
{
unchecked
{
int h = 0;
h = (h * 31) ^ _dgkComparer.GetHashCode(obj.DateKey);
h = (h * 31) ^ _valueComparer.GetHashCode(obj.ObjectKey);
return h;
}
}
}
Now my extension method(s) look like
public static class Extensions
{
// This was the original extension
public static IEnumerable<IGrouping<DateGroupKey, TValue>> GroupByDate<TValue>(
this IEnumerable<TValue> source,
Func<TValue, DateTime> dateSelector,
DateGroupType dateGroupType)
{
Func<TValue, DateGroupKey> keySelector = DateKeySelector(dateSelector, dateGroupType);
return source.GroupBy(keySelector, new DateGroupKeyComparer());
}
// New extension method supporting composite grouping
public static IEnumerable<IGrouping<DateGroupCompositeKey<TKey>, TValue>> GroupByDateComposite<TKey, TValue>(
this IEnumerable<TValue> source,
Func<TValue, DateTime> dateSelector,
Func<TValue, TKey> keySelector,
IEqualityComparer<TKey> keyComparer,
DateGroupType dateGroupType)
{
var dateKeySelector = DateKeySelector(dateSelector, dateGroupType);
DateGroupCompositeKey<TKey> func(TValue val) =>
new DateGroupCompositeKey<TKey>
{
DateKey = dateKeySelector(val),
ObjectKey = keySelector(val)
};
return source.GroupBy(func, new DateGroupCompositeKeyComparer<TKey>(keyComparer));
}
private static Func<TValue, DateGroupKey> DateKeySelector<TValue>(Func<TValue, DateTime> dateSelector, DateGroupType dateGroupType)
{
Func<TValue, DateGroupKey> dateKeySelector = null;
switch (dateGroupType)
{
case DateGroupType.Year:
dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year };
break;
case DateGroupType.Month:
dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month };
break;
case DateGroupType.Week:
dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Week = dateSelector(val).GetIso8601WeekOfYear() };
break;
case DateGroupType.Day:
dateKeySelector = val => new DateGroupKey { Year = dateSelector(val).Year, Month = dateSelector(val).Month, Day = dateSelector(val).Day };
break;
default:
throw new NotSupportedException($"Group type not supported: {dateGroupType}");
}
return dateKeySelector;
}
}
The extension can now be used (as in the original post, by grouping only by date)
var grouped = results.GroupBy<ProductDto>(x => x.TimeStamp, DateGroupType.Month);
or in the composite version
var grouped = results.GroupByDateComposite<int, ProductDto>(
x => x.TimeStamp,
x => x.ProductGroup,
EqualityComparer<int>.Default,
DateGroupType.Month);
The downside, as far as I can tell, is that for more complicated groupings, say
var grouped = results.GroupByDateComposite<CustomKey, ProductDto>(
x => x.TimeStamp,
x => new CustomKey{ ProductGroup = x.ProductGroup, Variant = x.Variant},
new CustomKeyComparer(),
DateGroupType.Month);
i would need to create seperate key classes and corresponding IEqualityComparer implementations to be able to do the grouping.

Getting List of Objects that occurs exaclty twice in a list

I have a List<CustomPoint> points; which contains close to million objects.
From this list I would like to get the List of objects that are occuring exactly twice. What would be the fastest way to do this? I would also be interested in a non-Linq option also since I might have to do this in C++ also.
public class CustomPoint
{
public double X { get; set; }
public double Y { get; set; }
public CustomPoint(double x, double y)
{
this.X = x;
this.Y = y;
}
}
public class PointComparer : IEqualityComparer<CustomPoint>
{
public bool Equals(CustomPoint x, CustomPoint y)
{
return ((x.X == y.X) && (y.Y == x.Y));
}
public int GetHashCode(CustomPoint obj)
{
int hash = 0;
hash ^= obj.X.GetHashCode();
hash ^= obj.Y.GetHashCode();
return hash;
}
}
based on this answer, i tried,
list.GroupBy(x => x).Where(x => x.Count() = 2).Select(x => x.Key).ToList();
but this is giving zero objects in the new list.
Can someone guide me on this?
You should implement Equals and GetHashCode in the class itself and not in the PointComparer
To get your code working, you need to pass an instance of your PointComparer as a second argument to GroupBy.
This method works for me:
public class PointCount
{
public CustomPoint Point { get; set; }
public int Count { get; set; }
}
private static IEnumerable<CustomPoint> GetPointsByCount(Dictionary<int, PointCount> pointcount, int count)
{
return pointcount
.Where(p => p.Value.Count == count)
.Select(p => p.Value.Point);
}
private static Dictionary<int, PointCount> GetPointCount(List<CustomPoint> pointList)
{
var allPoints = new Dictionary<int, PointCount>();
foreach (var point in pointList)
{
int hash = point.GetHashCode();
if (allPoints.ContainsKey(hash))
{
allPoints[hash].Count++;
}
else
{
allPoints.Add(hash, new PointCount { Point = point, Count = 1 });
}
}
return allPoints;
}
Called like this:
static void Main(string[] args)
{
List<CustomPoint> list1 = CreateCustomPointList();
var doubles = GetPointsByCount(GetPointCount(list1), 2);
Console.WriteLine("Doubles:");
foreach (var point in doubles)
{
Console.WriteLine("X: {0}, Y: {1}", point.X, point.Y);
}
}
private static List<CustomPoint> CreateCustomPointList()
{
var result = new List<CustomPoint>();
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 5; j++)
{
result.Add(new CustomPoint(i, j));
}
}
result.Add(new CustomPoint(1, 3));
result.Add(new CustomPoint(3, 3));
result.Add(new CustomPoint(0, 2));
return result;
}
CustomPoint implementation:
public class CustomPoint
{
public double X { get; set; }
public double Y { get; set; }
public CustomPoint(double x, double y)
{
this.X = x;
this.Y = y;
}
public override bool Equals(object obj)
{
var other = obj as CustomPoint;
if (other == null)
{
return base.Equals(obj);
}
return ((this.X == other.X) && (this.Y == other.Y));
}
public override int GetHashCode()
{
int hash = 23;
hash = hash * 31 + this.X.GetHashCode();
hash = hash * 31 + this.Y.GetHashCode();
return hash;
}
}
It prints:
Doubles:
X: 0, Y: 2
X: 1, Y: 3
X: 3, Y: 3
As you see in GetPointCount(), I create a dictionary per unique CustomPoint (by hash). Then I insert a PointCount object containing a reference to the CustomPoint which starts at a Count of 1, and every time the same point is encountered, the Count is increased.
Finally in GetPointsByCount I return the CustomPoints in the dictionary where PointCount.Count == count, in your case 2.
Please also note I updated the GetHashCode() method, since your one returns the same for point (1,2) and (2,1). If you do want that, feel free to restore your own hashing method. You will have to test the hashing function though, because it's hard to uniquely hash two numbers into one. That depends on the range of numbers used though, so you should implement a hash function that fits your own needs.

Processing ranking data w/ C# & LINQ

I have an object in a list that I need to rank several different ways. Currently the code is rather unwieldy as it requires me to individually address each column. Example:
public class Data
{
public int AValue { get; set; }
public int ARanking { get; set; }
public int BValue { get; set; }
public int BRanking { get; set; }
public int CValue { get; set; }
public int CRanking { get; set; }
}
public class Container
{
public List<Data> RankingData { get; set; }
public void RankData()
{
int count = 1;
foreach (Data item in RankingData.OrderBy(d => d.AValue))
{
item.ARanking = count;
count++;
}
count = 1;
foreach (Data item in RankingData.OrderBy(d => d.BValue))
{
item.BRanking = count;
count++;
}
count = 1;
foreach (Data item in RankingData.OrderBy(d => d.CValue))
{
item.CRanking = count;
count++;
}
}
}
The problem I am trying to solve is I want to write something roughly like this:
public void RankData<V, R>()
{
int count = 1;
foreach(Data item in RankingData.OrderBy(V))
{
item.R = count;
count++;
}
}
So that as I need to alter the ranking logic (for example, handle tie breaking rules) that I write the code once instead of copying the code 20 times to make the rules match. What am I missing?
UPDATE
Using Tanzelax's solution as a base this is the extension class I came up with:
public static class RankingExtension
{
public static void SetRanking<TKey>(this List<Data> dataSet, bool Ascending, Func<Data, TKey> getOrderBy, Action<Data, int> setRank)
where TKey : IComparable
{
var ordered = (Ascending) ? dataSet.OrderBy(getOrderBy) : dataSet.OrderByDescending(getOrderBy);
int i = 1;
foreach (Data item in ordered)
{
setRank(item, i);
i++;
}
}
}
I had to add in a switch so that I could control whether or not the field was being sorted ascending or not. And in my test scenarios it produces the appropriate output:
List<Data> test = new List<Data>();
test.Add(new Data { AValue = 25, BValue = 1.25, CValue = 99.99 });
test.Add(new Data { AValue = 89, BValue = 2.10, CValue = 1.01 });
test.Add(new Data { AValue = 10, BValue = 6, CValue = 45.45 });
test.Add(new Data { AValue = 15, BValue = 2.33, CValue = 2.99 });
test.Add(new Data { AValue = 90, BValue = 5.43, CValue = 27.89 });
test.SetRanking(false, d => d.AValue, (d, i) => d.ARank = i);
test.SetRanking(false, d => d.BValue, (d, i) => d.BRank = i);
test.SetRanking(true, d => d.CValue, (d, i) => d.CRank = i);
Not tested, but something like this:
void SetRanking(this List<Data> dataSet, Func<Data,int> getOrderBy, Action<Data,int> setRank)
{
var ordered = dataSet.OrderBy(getOrderBy).ToArray();
int i = i;
foreach (Data item in ordered)
{
setRank(item, i);
i++;
}
}
RankingData.SetRanking(d => d.AValue, (d,i) => d.ARanking = i);
RankingData.SetRanking(d => d.BValue, (d,i) => d.BRanking = i);
RankingData.SetRanking(d => d.CValue, (d,i) => d.CRanking = i);
This is similar to Tanzelax's answer but is a generic extension method.
public static void RankData<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Action<TSource, int> rankSetter
)
{
int count = 1;
foreach (var item in source.OrderBy(keySelector))
{
rankSetter(item, count);
++count;
}
}
It would be called similar to Tanzelax's answer also.
RankingData.RankData(d => d.AValue, (d,i) => d.ARanking = i);
Pass in a Func<Data,K> that returns the ranking key. K should implement IComparable
public static void Rank<K>( IEnumerable<Data> source, Func<Data,K> rankBy ) where K : IComparable
{
int count = 1;
foreach (var item in source.OrderBy( rankBy ))
{
item.R = count;
++count;
}
}

How do I use the IComparable interface?

I need a basic example of how to use the IComparable interface so that I can sort in ascending or descending order and by different fields of the object type I'm sorting.
Well, since you are using List<T> it would be a lot simpler to just use a Comparison<T>, for example:
List<Foo> data = ...
// sort by name descending
data.Sort((x,y) => -x.Name.CompareTo(y.Name));
Of course, with LINQ you could just use:
var ordered = data.OrderByDescending(x=>x.Name);
But you can re-introduce this in List<T> (for in-place re-ordering) quite easily; Here's an example that allows Sort on List<T> with lambda syntax:
using System;
using System.Collections.Generic;
class Foo { // formatted for vertical space
public string Bar{get;set;}
}
static class Program {
static void Main() {
List<Foo> data = new List<Foo> {
new Foo {Bar = "abc"}, new Foo {Bar = "jkl"},
new Foo {Bar = "def"}, new Foo {Bar = "ghi"}
};
data.SortDescending(x => x.Bar);
foreach (var row in data) {
Console.WriteLine(row.Bar);
}
}
static void Sort<TSource, TValue>(this List<TSource> source,
Func<TSource, TValue> selector) {
var comparer = Comparer<TValue>.Default;
source.Sort((x,y)=>comparer.Compare(selector(x),selector(y)));
}
static void SortDescending<TSource, TValue>(this List<TSource> source,
Func<TSource, TValue> selector) {
var comparer = Comparer<TValue>.Default;
source.Sort((x,y)=>comparer.Compare(selector(y),selector(x)));
}
}
Here's a simple example:
public class SortableItem : IComparable<SortableItem>
{
public int someNumber;
#region IComparable<SortableItem> Members
public int CompareTo(SortableItem other)
{
int ret = -1;
if (someNumber < other.someNumber)
ret = -1;
else if (someNumber > other.someNumber)
ret = 1;
else if (someNumber == other.someNumber)
ret = 0;
return ret;
}
#endregion
}
"That's great, but what if I want to be able to control the sort order, or sort by another field?"
Simple. All we need to do is add few more fields to the object. First we'll add a string for a different sort type and then we'll add a boolean to denote whether we're sorting in descending or ascending order and then add a field which determines which field we want to search by.
public class SortableItem : IComparable<SortableItem>
{
public enum SortFieldType { SortNumber, SortString }
public int someNumber = -1;
public string someString = "";
public bool descending = true;
public SortFieldType sortField = SortableItem.SortFieldType.SortNumber;
#region IComparable<SortableItem> Members
public int CompareTo(SortableItem other)
{
int ret = -1;
if(sortField == SortableItem.SortFieldType.SortString)
{
// A lot of other objects implement IComparable as well.
// Take advantage of this.
ret = someString.CompareTo(other.someString);
}
else
{
if (someNumber < other.someNumber)
ret = -1;
else if (someNumber > other.someNumber)
ret = 1;
else if (someNumber == other.someNumber)
ret = 0;
}
// A quick way to switch sort order:
// -1 becomes 1, 1 becomes -1, 0 stays the same.
if(!descending) ret = ret * -1;
return ret;
}
#endregion
public override string ToString()
{
if(sortField == SortableItem.SortFieldType.SortString)
return someString;
else
return someNumber.ToString();
}
}
"Show me how!"
Well since you asked so nicely.
static class Program
{
static void Main()
{
List<SortableItem> items = new List<SortableItem>();
SortableItem temp = new SortableItem();
temp.someString = "Hello";
temp.someNumber = 1;
items.Add(temp);
temp = new SortableItem();
temp.someString = "World";
temp.someNumber = 2;
items.Add(temp);
SortByString(items);
Output(items);
SortAscending(items);
Output(items);
SortByNumber(items);
Output(items);
SortDescending(items);
Output(items);
Console.ReadKey();
}
public static void SortDescending(List<SortableItem> items)
{
foreach (SortableItem item in items)
item.descending = true;
}
public static void SortAscending(List<SortableItem> items)
{
foreach (SortableItem item in items)
item.descending = false;
}
public static void SortByNumber(List<SortableItem> items)
{
foreach (SortableItem item in items)
item.sortField = SortableItem.SortFieldType.SortNumber;
}
public static void SortByString(List<SortableItem> items)
{
foreach (SortableItem item in items)
item.sortField = SortableItem.SortFieldType.SortString;
}
public static void Output(List<SortableItem> items)
{
items.Sort();
for (int i = 0; i < items.Count; i++)
Console.WriteLine("Item " + i + ": " + items[i].ToString());
}
}
If you want dynamic sort, you can use LINQ
var itemsOrderedByNumber = ( from item in GetClasses() orderby item.Number select item ).ToList();
var itemsOrderedByText = ( from item in GetClasses() orderby item.Text select item ).ToList();
var itemsOrderedByDate = ( from item in GetClasses() orderby item.Date select item ).ToList();
or "Sort" method of List class:
List<Class1> itemsOrderedByNumber2 = new List<Class1>( GetClasses() );
itemsOrderedByNumber2.Sort( ( a, b ) => Comparer<int>.Default.Compare( a.Number, b.Number ) );
List<Class1> itemsOrderedByText2 = new List<Class1>( GetClasses() );
itemsOrderedByText2.Sort( ( a, b ) => Comparer<string>.Default.Compare( a.Text, b.Text ) );
List<Class1> itemsOrderedByDate2 = new List<Class1>( GetClasses() );
itemsOrderedByDate2.Sort( ( a, b ) => Comparer<DateTime>.Default.Compare( a.Date, b.Date ) );
You can use this for sorting list
namespace GenaricClass
{
class Employee :IComparable<Employee>
{
public string Name { get; set; }
public double Salary { get; set; }
public int CompareTo(Employee other)
{
if (this.Salary < other.Salary) return 1;
else if (this.Salary > other.Salary) return -1;
else return 0;
}
public static void Main()
{
List<Employee> empList = new List<Employee>()
{
new Employee{Name="a",Salary=140000},
new Employee{Name="b",Salary=120000},
new Employee{Name="c",Salary=160000},
new Employee{Name="d",Salary=10000}
};
empList.Sort();
foreach (Employee emp in empList)
{
System.Console.Write(emp.Salary +",");
}
System.Console.ReadKey();
}
}
}
This might not be in relation to sorting order, but it is still - I think - an interesting use of IComparable:
public static void MustBeInRange<T>(this T x, T minimum, T maximum, string paramName)
where T : IComparable<T>
{
bool underMinimum = (x.CompareTo(minimum) < 0);
bool overMaximum = (x.CompareTo(maximum) > 0);
if (underMinimum || overMaximum)
{
string message = string.Format(
System.Globalization.CultureInfo.InvariantCulture,
"Value outside of [{0},{1}] not allowed/expected",
minimum, maximum
);
if (string.IsNullOrEmpty(paramName))
{
Exception noInner = null;
throw new ArgumentOutOfRangeException(message, noInner);
}
else
{
throw new ArgumentOutOfRangeException(paramName, x, message);
}
}
}
public static void MustBeInRange<T>(this T x, T minimum, T maximum)
where T : IComparable<T> { x.MustBeInRange(minimum, maximum, null); }
These simple extension methods allow you to do parameter range checking for any type that implements IComparable like this:
public void SomeMethod(int percentage, string file) {
percentage.MustBeInRange(0, 100, "percentage");
file.MustBeInRange("file000", "file999", "file");
// do something with percentage and file
// (caller will have gotten ArgumentOutOfRangeExceptions when applicable)
}
using System;
using System.Collections.Generic;
using System.Text;
namespace Sorting_ComplexTypes
{
class Program
{
static void Main(string[] args)
{
Customer customer1 = new Customer {
ID = 101,
Name = "Mark",
Salary = 2400,
Type = "Retail Customers"
};
Customer customer2 = new Customer
{
ID = 102,
Name = "Brian",
Salary = 5000,
Type = "Retail Customers"
};
Customer customer3 = new Customer
{
ID = 103,
Name = "Steve",
Salary = 3400,
Type = "Retail Customers"
};
List<Customer> customer = new List<Customer>();
customer.Add(customer1);
customer.Add(customer2);
customer.Add(customer3);
Console.WriteLine("Before Sorting");
foreach(Customer c in customer)
{
Console.WriteLine(c.Name);
}
customer.Sort();
Console.WriteLine("After Sorting");
foreach(Customer c in customer)
{
Console.WriteLine(c.Name);
}
customer.Reverse();
Console.WriteLine("Reverse Sorting");
foreach (Customer c in customer)
{
Console.WriteLine(c.Name);
}
}
}
}
public class Customer : IComparable<Customer>
{
public int ID { get; set; }
public string Name { get; set; }
public int Salary { get; set; }
public string Type { get; set; }
public int CompareTo(Customer other)
{
return this.Name.CompareTo(other.Name);
}
}

Categories