Sorting a List<> using linq in C# - c#

I would like to do "sorting" under Datagridview ColumnHeaderMouseClick.
Accending or Decending should need to be automatic, selected column value automatic.
I gone through many websites and tried some options but I could not be able to achieve my goal.
private void lst_Install_Item_Main_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
try
{
DataGridViewColumn newColumn = lst_Install_Item_Main.Columns[e.ColumnIndex];
List<test> temp = (List<test>)lst_Install_Item_Main.DataSource;
//var newList = temp.OrderBy(m => m.feet).ToList();
//var newList = temp.AsQueryable().OrderBy(m => m.feet).ToList();
temp.Sort((m1, m2) => m1.feet.CompareTo(m2.feet));
lst_Install_Item_Main.DataSource = temp;
lst_Install_Item_Main.Refresh();
}
catch (Exception ex)
{
MessageBox.Show("There was an error bringing sorting \n" + ex.Message, "Testing", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
Above code "sorting" the list on "feet" column but
I would like to pass the columnname which user clicked, is that possible ?
I have input like (1', 21', 123', 5', 10')
Above code sorting the list like >> (1', 10', 123', 21', 5')
But I want output like >> (1', 5', 10', 21', 123')
Is that possible to achieve ?
How to achieve Ascending or Descending here
(I mean while I clicked first time it will do Ascending and while click on same column second time it should need to do Descending)
Your suggestionshelp are greatly appreciated.

If you don't have negative values, then to sort as you want you need either to parse values as numeric or simply pad them:
temp.Sort((m1, m2) => m1.feet.PadLeft(2).CompareTo(m2.feet.PadLeft(2)));
When comparing strings "1", "5" and "10" this will be comparing " 1", " 5" and "10" instead (notice empty space, which is less than 0 character), making them sorted in the right order.
Make sure to chose number big enough to cover padding to the longest number.

You need to sort the values of feet as integers. To do this, you first need to remove the feet-symbol (') and afterwards parse the value to an int (temporarily, the value is still stored as a string).
This should do the trick:
temp.Sort((m1, m2) => int.Parse(m1.feet.Replace("'", "")).CompareTo(int.Parse(m2.feet.Replace("'", ""))));
Also, I would recommend that you don't store the feet-symbol in the value, and instead use some kind of formatting to include it, when showing the values in the Grid. That way you can avoid these kinds of conversions and comparisons, each time you need to use the values.

You have to convert your strings to integer-values as strings are differently ordered (lexicographically 10 comes before 2). Furtheremore as your input contains '-characters you have to delete them first using String.Trim('\'').
temp.Sort((m1, m2) => Convert.ToInt32(m1.feet.Trim('\'')).CompareTo(Convert.ToInt(m2.feet.Trim('\''))));
Alternativly you may also use Linq-OrderBy:
temp = temp.OrderBy(x => Convert.ToInt32(x.feet.Trim('\''))).ToList();
And OrderByDescending if in descending-order.

UPDATED for question # 3, added trigger for sort order
I would suggest you to use ICoparer in such way
public class TestComparer : IComparer<test>
{
bool isAscending;
public TestComparer(bool isAscendingOrder)
{
isAscending = isAscendingOrder;
}
int IComparer<test>.Compare(test a, test b)
{
int c1 = IntFromStr(a.feet);
int c2 = IntFromStr(b.feet);
int res;
res = (c1 > c2) ? 1 : (c1 < c2) ? -1 : 0;
return isAscending ? res : -res;
}
int IntFromStr(string s)
{
int result;
return (int.TryParse(s.Replace("'", ""), out result)) ? result : int.MaxValue;
}
}
this comparer will move not valid items to the end of the sorted list, also you will be able to change your sort behaviour easily, you will call it like below
List < test > lst = new List<test>();
// It will be your property to Trigger value each time you click
// (sortOrderTrigger = !sortOrderTrigger)
bool sortOrderTrigger = true;
lst.Add(new test { feet = "1'" });
lst.Add(new test { feet = "21'" });
lst.Add(new test { feet = "123'" });
lst.Add(new test { feet = "5'" });
lst.Add(new test { feet = "10'" });
lst.Add(new test { feet = "15'" });
lst.Add(new test { feet = "jj'" });
lst.Add(new test { feet = "ff'" });
lst.Sort(new TestComparer(sortOrderTrigger));

Thanks for all your help and Direction.
I have fixed my requirements as like below, Hope this help to someone like me :)
1. Created list which contains the same columns as the columns dataproperty.
public class sortList
{
public string Layer { get; set; }
public string mlenth { get; set; }
public string diameter { get; set; }
public string subtypecd { get; set; }
public string coatingtype { get; set; }
public string year { get; set; }
public string gisid { get; set; }
public string taxdistrict { get; set; }
public string lengthsource { get; set; }
public string shapelen { get; set; }
public string feet { get; set; }
public sortList()
{
this.Layer = ListSortDirection.Ascending.ToString();
this.mlenth = ListSortDirection.Ascending.ToString();
this.feet = ListSortDirection.Ascending.ToString();
this.diameter = ListSortDirection.Ascending.ToString();
this.subtypecd = ListSortDirection.Ascending.ToString();
this.coatingtype = ListSortDirection.Ascending.ToString();
this.year = ListSortDirection.Ascending.ToString();
this.gisid = ListSortDirection.Ascending.ToString();
this.taxdistrict = ListSortDirection.Ascending.ToString();
this.lengthsource = ListSortDirection.Ascending.ToString();
this.shapelen = ListSortDirection.Ascending.ToString();
}
}
2. Written code on ColumnHeaderMouseClick event and order function
private void lst_Install_Item_Main_ColumnHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
{
try
{
//Get Column which clicked
DataGridViewColumn newColumn = lst_Install_Item_Main.Columns[e.ColumnIndex];
//Get Values from DataGrid
List<test> temp = (List<test>)lst_Install_Item_Main.DataSource;
//Get the sorting order for that column
string orderby = GetOrder(newColumn.DataPropertyName);
if (orderby == ListSortDirection.Ascending.ToString()) //Ascending
{
if (newColumn.DataPropertyName == "feet") //Feet column sort with double value and required trim
{
temp = temp.OrderBy(x => Convert.ToDouble(x.feet.Trim('\''))).ToList();
}
else if (newColumn.DataPropertyName == "shapelen") //Shapelen column sort with double value and required trim
{
temp = temp.OrderBy(x => Convert.ToDouble(x.shapelen.Trim('\''))).ToList();
}
else ///other columns having string value only.
{
temp = temp.OrderBy(x => x.GetType().GetProperty(newColumn.DataPropertyName).GetValue(x, null)).ToList();
}
}
else // Descending
{
if (newColumn.DataPropertyName == "feet") //Feet column sort with double value and required trim
{
temp = temp.OrderByDescending(x => Convert.ToDouble(x.feet.Trim('\''))).ToList();
}
else if (newColumn.DataPropertyName == "shapelen") //Shapelen column sort with double value and required trim
{
temp = temp.OrderByDescending(x => Convert.ToDouble(x.shapelen.Trim('\''))).ToList();
}
else //other columns having string value only.
{
temp = temp.OrderByDescending(y => y.GetType().GetProperty(newColumn.DataPropertyName).GetValue(y, null)).ToList();
}
}
lst_Install_Item_Main.DataSource = temp;
lst_Install_Item_Main.Refresh();
}
catch (Exception ex)
{
MessageBox.Show("There was an error while sorting \n" + ex.Message, "Closeout Calculator", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private string GetOrder(string columnName)
{
//Store the each coulmn(Ascending/Descending) values to make it dynamic
string ord = sortOrder[0].GetType().GetProperty(columnName).GetValue(sortOrder[0], null).ToString();
if (ord == ListSortDirection.Ascending.ToString())
{
sortOrder[0].GetType().GetProperty(columnName).SetValue(sortOrder[0], ListSortDirection.Descending.ToString(), null);
}
else
{ sortOrder[0].GetType().GetProperty(columnName).SetValue(sortOrder[0], ListSortDirection.Ascending.ToString(), null); }
return ord;
}
3. sortoder list initialize and obj declared at Constructor.
Private List<sortList> sortOrder = new List<sortList>();
sortOrder.Add(new sortList());

Related

What is the best way to return duplicate values from a list on the same ListView row in the GetView() Method?

I have some data in a list that shows for example the Item Number, Line Number, Quantity, and Pack Size of an order. The problem is that some lines on the order will have the same item number but different quantities, pack sizes or both. For example :
Order# Line# Item# Qty Pack Size
100 1 12345 640 (10#64)
100 1 12345 128 (1#128)
100 2 23124 48 (1#48)
100 3 53425 80 (1#80)
Shown above for item 12345 they have ordered a total of 768 pieces but for 640 of those pieces they would like to receive 10 packs containing 64 pieces each and they would like 128 of the pieces in just one pack. When the ListView is created naturally it will create 4 rows, one row for each of the rows in the list. I would like it to only show 3 rows one for each Item# and if there are duplicate items it should add the quantities and combine the pack sizes into one row. I have tried doing something like this trying to force it to do what i wanted but it obviously does not work that well. I am fairly new to Android and ListViews so I am hoping that there is a better way to accomplish this. Can anyone point me in the right direction?
public class LoadInfoViewAdapter : BaseAdapter<Items>
{
private List<Items> lItems;
private Context gContext;
private string gLoadnStop;
private string gPacks;
private decimal gtotal;
int total;
public LoadInfoViewAdapter(Context context, List<Items> loadinfo, string LoadnStop)
{
lItems = loadinfo;
gContext = context;
gLoadnStop = LoadnStop;
}
public override int Count
{
get { return lItems.Count; }
}
public override long GetItemId(int position)
{
return position;
}
public override Items this[int position]
{
get { return lItems[position]; }
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View row = convertView;
if (row == null)
{
row = LayoutInflater.From(gContext).Inflate(Resource.Layout.LoadInfoItems_Row, null);
}
//If the item at the current position is the same as the item in the next position add the qty and combine the packs
if (lItems[position].ItemNo == lItems[position + 1].ItemNo)
{
string itemno = lItems[position].ItemNo;
int count = 0;
row.FindViewById<TextView>(Resource.Id.txtItemNo).Text = lItems[position].ItemNo.Trim();
row.FindViewById<TextView>(Resource.Id.txtLineNo).Text = lItems[position].LineNum.Trim();
foreach (var item in lItems.Where(r => r.ItemNo == itemno))
{
if (count == 0)
{
gPacks = lItems[position].Pack.ToString().Trim();
gtotal = lItems[position].Qty;
}
else
{
gPacks = lItems[position + 1].Pack.ToString().Trim() + ", " + gPacks;
gtotal = lItems[position + 1].Qty + gtotal;
}
count = +1;
}
row.FindViewById<TextView>(Resource.Id.txtQuantity).Text = gtotal + " (" + gPacks + ") ";
gtotal = 0;
}
//check to see if the current item is was the same as the previous item added. If so I dont want to create a row for that item.
else if (lItems[position].ItemNo == lItems[position - 1].ItemNo)
{
//Not too sure what would be the best code to go here
}
//if it has no duplicate create the row like normal
else
{
row.FindViewById<TextView>(Resource.Id.txtItemNo).Text = lItems[position].ItemNo.Trim();
row.FindViewById<TextView>(Resource.Id.txtLineNo).Text = lItems[position].LineNum.Trim();
row.FindViewById<TextView>(Resource.Id.txtQuantity).Text = lItems[position].Qty.ToString().Trim() + lItems[position].Pack.ToString().Trim();
}
return row;
}
I would like it to return something like this :
12345 1 768((10#64),(1#128))
23124 2 48(1#48)
53425 3 80(1#80)
For whatever reason it will do what i was hoping for the duplicate item numbers but it sometimes duplicates a different item in the list like:
12345 1 768((10#64),(1#128))
23124 2 48(1#48)
53425 3 80(1#80)
23124 2 48(1#48)
Anyone got any suggestions?
Your fundamental issue is that the Items indexer is stating that you have 4 items available, but your desired result is 3 items.
This isn't really a task that you should be solving in your BaseAdapter. You should be processing the list in advance and reducing to your target 3 items BEFORE passing it into your base adapter.
Also, you should try and avoid using FindViewFromId in GetView, unless the convertView is null. In order to do this, you can pack the previously identified Views into the Tag property: that's what it's for, see https://blog.xamarin.com/creating-highly-performant-smooth-scrolling-android-listviews/
If you really want to do it all in your base adapter, then convert the items in the constructor, something like this:
public class LoadInfoViewAdapter : BaseAdapter<Items>
{
private List<CondensedItems> cItems;
private Context gContext;
public LoadInfoViewAdapter(Context context, List<Items> loadinfo, string LoadnStop)
{
gContext = context;
gLoadnStop = LoadnStop;
// Get all of the unique item numbers.
var uniqueItemNos = loadInfo.Select(x => x.ItemNo).Distinct();
cItems = new List<CondensedItem>();
foreach(string uniqueItem in uniqueItemNos)
{
// Gets all of the items with the target itemNo. If you want to avoid merging them all, you need to do some work checking for sequential indexes or something similar.
var matchingItems = loadInfo.Select(x => x.ItemNo == uniqueItem).ToList();
var lineNo = matchingItems[0].LineNo;
var qty = matchingItems.Sum(x => x.Qty).ToString();
StringBuilder packsSB = new StringBuilder();
foreach(var item in matchingItems)
{
packs.Append(item.Pack + ",");
}
string packs = packsSB.ToString().Trim(',');
cItems.Add(
new CondensedItem
{
ItemNo = uniqueItem,
LineNo = lineNo,
Packs = $"{qty}({packs})"
});
}
}
public override int Count
{
get { return cItems.Count; }
}
public override long GetItemId(int position)
{
return position;
}
public override Items this[int position]
{
get { return cItems[position]; }
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
View row = convertView;
CondensedViewHolder viewHolder;
if (row == null)
{
row = LayoutInflater.From(gContext).Inflate(Resource.Layout.LoadInfoItems_Row, null);
viewHolder = new CondensedViewHolder
{
ItemView = row.FindViewById<TextView>(Resource.Id.txtItemNo),
LineView = row.FindViewById<TextView>(Resource.Id.txtLineNo),
PacksView = row.FindViewById<TextView>(Resource.Id.txtQuantity)
}
row.Tag = viewHolder;
}
else
{
viewHolder = row.Tag as CondensedViewHolder;
}
viewHolder.ItemView.Text = cItems[position].ItemNo;
viewHolder.LineView.Text = cItems[position].LineNo;
viewHolder.PacksView.Text = cItems[position].Packs;
return row;
}
private class CondensedItem
{
string ItemNo { get; set; }
string LineNo { get; set; }
string Packs { get; set; }
}
private class CondensedViewHolder : Java.Lang.Object
{
public TextView ItemView { get; set; }
public TextView LineView { get; set; }
public TextView PacksView { get; set; }
}
}

Get top 5 scores/names from list

I am adding a function to keep track of scores for a small game I made.
I want to get the top 5 scores (including name for that score) from a file that contains the scores.
The format of the saved scores is:
[name]-[score]
The scores and names are stored in 2 lists, which I parse this way:
string scores = File.ReadAllText(Environment.GetEnvironmentVariable("TEMP") + "/scores");
string[] splitscores = scores.Split('\n');
foreach (string entry in splitscores)
{
string replace = entry.Replace("[", "").Replace("]", "");
string[] splitentry = replace.Split('-');
if (splitentry.Count() > 1)
{
scorenames.Add(splitentry[0]);
scorelist.Add(Int32.Parse(splitentry[1]));
}
}
Then I retrieve #1 player by using:
int indexMax
= !scorelist.Any() ? -1 :
scorelist
.Select((value, index) => new { Value = value, Index = index })
.Aggregate((a, b) => (a.Value > b.Value) ? a : b)
.Index;
lblhighscore1.Text = "#1: " + scorelist[indexMax] + " by " + scorenames[indexMax];
How can I set the remaining 4 players assuming this is my scorelist:
[broodplank]-[12240]
[flo]-[10944]
[bill]-[11456]
[tony]-[9900]
[benji]-[7562]
I've figured I could do a descending sort of the score list, but that wouldn't cover the changes in the indexes of the usernames list, what is the best approach for this?
Best approach? Don't use parallel collections anti-pattern.
Instead of having 2 lists, create a class that can hold both the name and the score together
class Score
{
public string Name { get; private set; }
public int Score { get; private set; }
public Score(string name, int score)
{
Name = name;
Score = score;
}
}
and have just one list
List<Score> scores = new List<Score>();
foreach (string entry in splitscores)
{
string replace = entry.Replace("[", "").Replace("]", "");
string[] splitentry = replace.Split('-');
if (splitentry.Count() > 1)
{
scores.Add(new Score(splitentry[0], Int32.Parse(splitentry[1]))
}
}
You can easily order by one property and because the whole object will be reordered you'll keep the names in the right order without any additional code:
topScores = scores.OrderByDescending(x => x.Score).Take(5);
In addition to MarcinJuraszeks useful answer, some small things that I came across using his solution which I decided to share.
First problem was with the class which threw me the following error
'Score': member names cannot be the same as their enclosing type
Changing the case of "s" fixed it
class Score
{
public string Name { get; private set; }
public int score { get; private set; }
public Score(string name, int Score)
{
Name = name;
score = Score;
}
}
And calling the individual values can be done with Linq
string numberOne = topScores.Skip(0).First().score
string numberTwo = topScores.Skip(1).First().score
and so on

How to sort data in gridview?

I am trying to sort the data in gridview
everything is working fine but numeric column(Marks) taking sorting for 1st number only
Code:
protected void gvTrHty_Sorting(object sender, GridViewSortEventArgs e)
{
try
{
this.gviewSorting(e.SortExpression);
}
catch (Exception ex)
{
string arg_15_0 = ex.Message;
}
}
private void gviewSorting(string strSortExp)
{
if (this.ViewState["dvTrain"] == null)
{
DataSet dataSet = this.BindTraining();
dv = dataSet.Tables[0].DefaultView;
}
else
{
DataSet dataSet2 = (DataSet)this.ViewState["dvTrain"];
TrainingHistory.dv = dataSet2.Tables[0].DefaultView;
}
if (TrainingHistory.sortorder)
{
TrainingHistory.sortorder = false;
TrainingHistory.dv.Sort = strSortExp + " DESC";
}
else
{
TrainingHistory.sortorder = true;
TrainingHistory.dv.Sort = strSortExp;
}
this.BindData(TrainingHistory.dv);
}
If I have values in Mark(column) in gridview
Marks----> When I click this for sorting it's taking Marks
1 1
8 1st number only sorted ---> 12
40 21
12 40
21 8
It is treating your "numerical" data as a string and doing the sort against this string value, thus "40" is less than "8".
Your options are:
Put leading zeroes on the numeric field values, which is probably a no-go for obvious reasons, this would allow the existing sort to work correctly. I suppose you could temporarily put leading zeroes and then rip them out after the sort, but this sounds like a headache.
Implement your own sorting logic, like this:
public sealed class GenericComparer<T> : IComparer<T>
{
public enum SortOrder
{
Ascending = 0,
Descending = 1
}
private string sortColumn;
private SortOrder sortingOrder;
public string SortColumn
{
get
{
return this.sortColumn;
}
}
public SortOrder SortingOrder
{
get
{
return this.sortingOrder;
}
}
public GenericComparer(string theSortColumn, SortOrder theSortingOrder)
{
this.sortColumn = theSortColumn;
this.sortingOrder = theSortingOrder;
}
public int Compare(T x, T y)
{
PropertyInfo thePropertyInfo = typeof(T).GetProperty(this.sortColumn);
IComparable object1 = (IComparable)thePropertyInfo.GetValue(x, null);
IComparable object2 = (IComparable)thePropertyInfo.GetValue(y, null);
if (this.sortingOrder == SortOrder.Ascending)
{
return object1.CompareTo(object2);
}
else
{
return object2.CompareTo(object1);
}
}
}
Now in your call to .Sort() method, you pass a new instance of this helper class (passing it the column you want to sort by and the direction you want to sort - ascending or descending).
Since the comparer logic above uses generics, you can pass whatever type you want to sort by (i.e. int, DateTime, or even entire domain objects).

Searching Country by Country code

I am working on a search method, which will be called with Ajax, and updates a Webgrid in Mvc4.
The search will go through a list of Project objects, which contains some fields.
One of the fields is Country. And right now my code only checks if the input string contains the search string:
private bool StringStartWith(string input, string searchstring)
{
bool startwith = false;
var inputlist = new List<string>(input.ToLower().Split(' ').Distinct());
var searchList = new List<string>(searchstring.ToLower().Split(' '));
var count = (from inp in inputlist from sear in searchList where inp.StartsWith(sear) select inp).Count();
if (count == searchList.Count)
startwith = true;
return startwith;
}
But I also want to be able to search by country code. So if I write "DK", it should tell that it is equal to Denmark.
I hope I can get some help for it.
Thanks.
//UPDATE!!
iTURTEV answer helped me to make my method work as it should. I just had to update my method as shown here:
private bool InputStartWithSearch(string input, string searchstring)
{
if(searchstring[searchstring.Length-1].Equals(' '))
searchstring = searchstring.Substring(0,searchstring.Length-2);
bool startwith = false;
var inputlist = new List<string>(input.ToLower().Split(' ').Distinct());
var searchList = new List<string>(searchstring.ToLower().Split(' '));
if (searchstring.Length == 2)
{
var countryCode = new RegionInfo(searchstring.ToUpper()).EnglishName;
if (inputlist.Any(country => country.ToLower().Equals(countryCode.ToLower())))
{
return true;
}
}
var count = (from inp in inputlist from sear in searchList where inp.StartsWith(sear) select inp).Count();
if (count == searchList.Count)
startwith = true;
return startwith;
}
Thanks a lot.
May be you can use RegionInfo:
// returns Bulgaria
new RegionInfo("BG").EnglishName;
Assuming:
public class Country {
public int Id { get; set; }
public string Name { get; set; }
public string IsoCode { get; set; }
}
Then:
return x.Countries.Where(q =>
q.Name != null && q.Name.ToLowerInvariant().Contains(text) ||
q.IsoCode != null && q.IsoCode.ToLowerInvariant().Contains(text));
This will return every Country having text on its name or code. It's important to check for nulls unless you're using [Required] data annotation, if you don't want this to be case insensitive you could remove the .ToLowerInvariant().

Combine elements of my List

I got following class:
public class Action
{
public Player player { get; private set; }
public string type { get; private set; }
public decimal amount { get; private set; }
}
Which is used in a List:
public List<Action> Action
Depending on type I display some custom text. But if type = "folds" I just display 1 Folds. If there are many folds after another, it currently displays:
1 folds, 1 folds, 1 folds, ...
How can I combine those folds in a smart way and display it like this:
3 folds, ...
Just make a counter for the folds, reset it when you hit a fold, increment until you hit a non-fold, then output it before doing the current action. Anything else is inefficient and, honestly, overthinking the issue.
int counter = 0;
foreach Action currAction in Action
{
if (currAction.Type == "fold")
{
++counter;
}
else
{
if (counter > 0)
{
\\ print it out and reset to zero
}
DoStuff();
}
}
List<Action> actions = …
Console.WriteLine("{0} folds", actions.Sum(a => a.type == "folds" ? 1 : 0));
You can use linq to group the elements by type, then process these groups to get the desired output:
var actionGroups = actions.GroupBy(a => a.type);
IEnumerable<string> formattedActions = actionGroups
.Select(grp => new[] { Type = grp.Key, Count = grp.Count})
.Select(g => String.Format("{0} {1}{2}", g.Count, g.Type, g.Count == 1 ? "s" : String.Empty));
You could use helper class like this:
public class ActionMessages : IEnumerable<string>
{
private IEnumerable<Action> actions;
public IEnumerator<string> GetEnumerator()
{
int foldCount = 0;
foreach(var action in this.actions) {
if (action.type=='fold')
foldCount++;
else {
if (foldCount>0)
yield return foldCount.ToString() + " folds";
foldCount = 0;
yield return action.ToString();
}
}
if (foldCount>0)
yield return foldCount.ToString() + " folds";
}
// Constructors
public ActionMessages (IEnumerable<Action> actions)
{
this.actions = actions;
}
}

Categories