Introduction
How many of you have a list that contains complex values and want to sort it based on ASCII code and the actual value? I would say this article will certainly solve your problem.
I will try to explain the solution based on a simple scenario.
Background
One of users requirement was to be able to sort and view their plot records, which contains the plot number, area code, area and perimeter, and other attributes. The plot number contains a series of characters that may contain letters, numbers, and one or more non-alphanumeric characters like forward slash (/), backward slash(\), dash(-), or an underscore ( _ ).
So the sorting requirement was, a number is preceded by a letter or non-alphanumeric character, and the number should be sorted by the actual value of the number and the rest of the characters. For example:
- P-1054/A
- P-100/B
- P-807/A
- P-1083/A
- P-20/B
After sorted would be:
- P-20/B
- P-100/B
- P-807/A
- P-1054/A
- P-1083/A
Problem
To solve this requirement, let’s investigate the default Sort
method and property of the List<T>
generic collection class and the DataView
class, respectively. Both give us a sorted list, but don’t give the required result. If we sort the previous example, the output will be:
- P-100/B
- P-1054/A
- P-1083/
- P-20/B
- P-807/A
Solution
Among one of the solutions is traversing each character and compare its value based on its number, letter, or special character value. So if the object to be compared contains a number, then compare as a number value. I will explain how to do this in the next section.
Note: For this article, I will consider only the List<T>
class.
Using the Code
One of the built-in comparison interfaces that .NET provides is the IComparer<T>
interface, which is a generic type. Implementing this interface in a Comparer
class, which I call CharacterComparer<T>
, gives us a generic Compare
method. The class considers which attribute of the Type T
is going to be used for comparing the values. It is also possible to make the comparison case-sensitive. I also use Reflection
to get the attribute value of the Type T
so that the actual character comparison can be done easily. A portion of the sorting class code is shown below:
namespace SortLibray
{
public class CharacterComparer<T> : IComparer<T>
{
public int Compare(T left, T right)
{
char charLeft = leftValue[indicatorLeft];
char charRight = rightValue[indicatorRight];
char[] spaceLeft = new char[lengthLeft];
char[] spaceRight = new char[lengthRight];
int lockerLeft = 0,lockerRight = 0;
do
{
spaceLeft[lockerLeft] = charLeft;
lockerLeft = lockerLeft + 1;
indicatorLeft = indicatorLeft + 1;
if (indicatorLeft < lengthLeft)
charLeft = leftValue[indicatorLeft];
else
break;
} while (char.IsDigit(charLeft) == char.IsDigit(spaceLeft[0]));
}
}
}
The core of the comparison logic is to traverse each character of the left and right attribute values of Type T
objects. We can also extend this to comply with each attribute of a specific class, say Plot
(I chose this class because I already mentioned it in the background of this article).
namespace SortDemo
{
public class Plot
{
public Plot()
{
}
public Plot(string plotNumber, int? areaCode,
float? area, float? perimeter)
{
this.PlotNumber = plotNumber;
this.AreaCode = areaCode;
this.Area = area;
this.Perimeter = perimeter;
}
}
}
The extended PlotNumberComparer
class will now look like this:
namespace SortDemo
{
public class PlotNumberComparer : CharacterComparer<Plot>
{
public PlotNumberComparer()
:base("PlotNumber")
{
}
public PlotNumberComparer(bool caseSensitive)
: base("PlotNumber",caseSensitive)
{
}
}
}
Let us see how we can demonstrate this comparer capability. I chose ASP.NET for the demo with a GridView
control. Before going into that, let's make a collection class for the Plot
class called Plots
and an extension class for the List<T>
generic collection class called SortExtension
that will comply with the GridView
control.
Here is the Plots
class:
namespace SortDemo
{
public class Plots : List<Plot>
{
public Plots()
{
this.Add(new Plot("P-1054/A", 3001, null, 105.081f));
this.Add(new Plot("P-100/B", 01, 734.156f, null));
this.Add(new Plot("P-807/A", 3001, 764.277f, 111.299f));
this.Add(new Plot("P-20/B", 01, 734.156f, 108.945f));
this.Add(new Plot("P-1083/A", 3001, 198.52f, 68.108f));
}
}
}
Here is the SortExtension
class:
namespace SortDemo
{
public static class SortExtension
{
public static IList<T> SortedList<T>(this List<T> genericList,
string sortExpression, SortDirection sortDirection,
CharacterComparer<T> comparer = null, bool caseSensitive = false)
{
if (genericList == null ||
string.IsNullOrEmpty(sortExpression) ||
string.IsNullOrWhiteSpace(sortExpression))
return null;
else
{
if (comparer == null)
if (caseSensitive)
comparer = new CharacterComparer<T>(sortExpression, caseSensitive);
else
comparer = new CharacterComparer<T>(sortExpression);
else
if (caseSensitive)
if (!comparer.CaseSensitive)
comparer.CaseSensitive = caseSensitive;
genericList.Sort(comparer);
if (sortDirection == SortDirection.Descending)
genericList.Reverse();
}
return genericList;
}
public static IList<T> SortedList<T>
(this List<T> genericList, SortDirection sortDirection,
CharacterComparer<T> comparer, bool caseSensitive = false)
{
if (caseSensitive)
if (!comparer.CaseSensitive)
comparer.CaseSensitive = caseSensitive;
genericList.Sort(comparer);
if (sortDirection == SortDirection.Descending)
genericList.Reverse();
return genericList;
}
public static IList<T> SortedList<T>
(this List<T> genericList, string sortExpression, SortDirection sortDirection,
bool caseSensitive = false)
{
return SortedList(genericList, sortExpression,
sortDirection, null, caseSensitive);
}
}
}
Now most of the things have been done well so far. Let's put them in the presentation layer. In our presentation code class called GridViewSortDemo.aspx.cs, let's put a couple of methods and properties that will ease our life to present the demo.
Here are some page properties:
public string GridViewSortExpression
{
get
{
if (ViewState[Constants.SORT_EXPRESSION] != null)
return ViewState[Constants.SORT_EXPRESSION].ToString();
return Constants.PLOT_NUMBER;
}
set
{
ViewState[Constants.SORT_EXPRESSION] = value;
}
}
public SortDirection GridViewSortDirection
{
get
{
if (ViewState[Constants.SORT_DIRECTION] != null)
return (SortDirection)ViewState[Constants.SORT_DIRECTION];
return SortDirection.Ascending;
}
set
{
ViewState[Constants.SORT_DIRECTION] = value;
}
}
Here are some private
methods:
private void bindGridView()
{
try
{
Plots plots = new Plots();
if (cbCharactorComparer.Checked)
{
if (GridViewSortExpression.ToLower().Equals(Constants.PLOT_NUMBER.ToLower()))
plots.SortedList(GridViewSortDirection,
new PlotNumberComparer(), cbCaseSensitivity.Checked);
else
plots.SortedList(GridViewSortExpression,
GridViewSortDirection, cbCaseSensitivity.Checked);
gvPlots.DataSource = plots;
gvPlots.DataBind();
}
else
{
List<Plot> sortResult = plots;
switch (GridViewSortExpression)
{
case Constants.PLOT_NUMBER:
sortResult = plots.OrderBy(p => p.PlotNumber).ToList();
break;
case Constants.AREA_CODE:
sortResult = plots.OrderBy(p => p.AreaCode).ToList();
break;
case Constants.AREA:
sortResult = plots.OrderBy(p => p.Area).ToList();
break;
case Constants.PERIMETER:
sortResult = plots.OrderBy(p => p.Perimeter).ToList();
break;
default:
sortResult = plots.OrderBy(p => p.PlotNumber).ToList();
break;
}
if (GridViewSortDirection == SortDirection.Descending)
sortResult.Reverse();
gvPlots.DataSource = sortResult;
gvPlots.DataBind();
}
}
catch(Exception exception)
{
Response.Write(exception.Message);
}
}
Finally, let’s call the method bindGridView()
to the Page_Load
event:
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
bindGridView();
}
Now everything is presented well and ready to be tested. Just download the fully functioning application and run it. Enjoy. :)
History
- Jan. 17, 2011: First version
- Feb. 27, 2012: Updated version
- Feb. 28, 2012: Updated version