C# all common sequences in lists of strings - c#

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);

Related

How To get Positional Occurrence of strings in List using Linq?

var a = new List<string>() {"aa","aa","bb","bb","bb","cc","aa","aa","cc","cc" };
I want to find occurrence count of all strings, for above list i want output like:
Index String Count
0 aa 2
2 bb 3
5 cc 1
6 aa 2
8 cc 2
but as i tried groupby it gives the total count not positional.
how i can get this?
edit: i expect more than 10 million entries in list.
You can do following.
Solution Using Linq
var a = new List<string>() {"aa","aa","bb","bb","bb","cc","aa","aa","cc","cc" };
var result = Enumerable.Range(0, a.Count())
.Where(x => x == 0 || a[x - 1] != a[x])
.Select((x,index) => new Stat<string>
{
Index =index,
StringValue = a[x],
Count = a.Skip(x).TakeWhile(c => c == a[x]).Count()
});
Where Stat is defined as
public class Stat<T>
{
public int Index{get;set;}
public T StringValue {get;set;}
public int Count {get;set;}
}
Update
Using Iterator
public IEnumerable<Stat<T>> CountOccurance<T>(IEnumerable<T> source)
{
var lastItem = source.First();
var count = 1;
var index= 0;
foreach(var item in source.Skip(1))
{
if(item.Equals(lastItem))
{
count++;
}
else
{
yield return new Stat<T>
{
Index = index,
StringValue = lastItem,
Count = count
};
count=1;
lastItem = item;
index++;
}
}
yield return new Stat<T>
{
Index = index,
StringValue = lastItem,
Count = count
};
}
You can then fetch the result as
var result = CountOccurance(a);
If you will use this logic multiple times you can consider writing your own extension method.
public static class ExtensionMethods
{
public static IEnumerable<SpecialGroup<TKey>> GroupAccordingToSuccessiveItems<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
int index = 0;
int count = 0;
TKey latestKey = default(TKey);
foreach (var item in source)
{
TKey key = keySelector(item);
if (index != 0 && !object.Equals(key, latestKey))
{
yield return new SpecialGroup<TKey>
{
Index = index - count,
Obj = latestKey,
Count = count
};
count = 0;
}
latestKey = key;
count++;
index++;
}
yield return new SpecialGroup<TKey>
{
Index = index - count,
Obj = latestKey,
Count = count
};
}
}
public class SpecialGroup<T>
{
public int Index { get; set; }
public T Obj { get; set; }
public int Count { get; set; }
}
Then you can call it,
var result =
a.
GroupAccordingToSuccessiveItems(i => i);
This will iterate all the list once.

String split to object with Linq?

Given:
class T
{
public string A { get; set; }
public string B { get; set; }
}
string s = "A|B";
Is there a way to split s on the | and return a T object "inline"? I know I can do something like:
s.Select(x => { string[] arr = s.Split(); return new T() { A = arr[0], B = arr[1] };
But I'm wondering if there is some obscure linq thing to do it "inline" without declaring the array and splitting inside of the select. Something more along the lines of:
s.Split().Select(x => new T() { A = x[0], B = x[1] });
Obviously that'll give you a compiler error, but you get the idea... is there a way to do it like that?
If you want to do it in one line, sure:
var s = "A|B";
var t = new T{ A = s.Split('|')[0], B = s.Split('|')[1] };
But obviously that uses Split twice and it looks bad.
This is probably a sign that you need a method:
private static T ParseT(string s) {
// do the conversion *properly* here
}
Then you can just call it:
ParseT("A|B")
Alternatively, add an explicit (recommended) or implicit conversion:
public static explicit operator T(string s) {
// do the conversion *properly* here
}
If you use query syntax then you could do something like this:
var strings=new string[] { "A|B","C|D"};
var query= from s in strings
let x=s.Split('|')
select new T{ A = x[0], B = x[1] };
Update
If "A|B" is the data source I don't recommend use Linq for that, you just could do:
var arr= str.Split('|');
var instance=new T{A = arr[0],B=arr[1]};
Or do the same in the constructor as #James recommended in his answer.
It's not really better than what you originally had, but this is one way (if s is a IEnumerable<string>:
s.Select(x=>x.Split('|')).Select(x=>new T{A=x[0],B=x[1]});
if s is a single string, then you would do:
new List<string>{s} // Now List<string> with 1 string in the list
.Select(x=>x.Split('|')) // now IEnumerable<string[]> with 1 string array in it
.Select(x=>new T{A=x[0],B=x[1]}) // now IEnumerable<T> with 1 T in it
.First(); // Now just T
Why fight actual language concepts:
void Main()
{
var strs = new List<string> { "A|B", "CCC|DD", "E|FFF"};
var Ts = strs.Select(s =>s.ToT() );
Ts.Dump();
}
static class Ext
{
static public T ToT(this string str)
{
return new T(str);
}
}
public class T
{
public string A { get; set; }
public string B { get; set; }
public T(string str)
{
var arr= str.Split('|');
A = arr[0];
B = arr[1];
}
}
NOTE I do not recommend doing it this way this was more of a fun way to do.
public static T CreateLinq(string s)
{
return s.Aggregate((a: new StringBuilder(), b: new StringBuilder(), c: false),
(acc, c) => (a: (!acc.c && c != '|' ? acc.a.Append(c) : acc.a),
b: (acc.c && c != '|' ? acc.b.Append(c) : acc.b),
c: acc.c || c == '|'),
acc => new T { A = acc.a.ToString(), B = acc.b.ToString() });
}
And the performance is not as bad as it seams on the first glance.
public class T
{
public string A { get; set; }
public string B { get; set; }
};
static void Main(string[] args)
{
var s1 = Enumerable.Range(0, 1000000).Aggregate(new StringBuilder(), (acc, i) => acc.Append("A")).ToString();
var s2 = Enumerable.Range(0, 1000000).Aggregate(new StringBuilder(), (acc, i) => acc.Append("B")).ToString();
var text =$"{s1}|{s2}";
for (int i = 0; i < 5; i++)
{
Stopwatch sw = new Stopwatch();
Console.WriteLine("Start");
sw.Start();
var t1 = CreateT(text);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Reset();
sw.Start();
var t2 = CreateLinq(text);
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
sw.Reset();
}
Console.ReadLine();
}
public static T CreateLinq(string s)
{
return s.Aggregate((a: new StringBuilder(), b: new StringBuilder(), c: false),
(acc, c) => (a: (!acc.c && c != '|' ? acc.a.Append(c) : acc.a),
b: (acc.c && c != '|' ? acc.b.Append(c) : acc.b),
c: acc.c || c == '|'),
acc => new T { A = acc.a.ToString(), B = acc.b.ToString() });
}
public static T CreateT(string s)
{
var split = s.Split('|');
return new T { A = split[0], B = split[1] };
}

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;
}
}

C# Ranking of objects, multiple criteria

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; }
}

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