Group objects list by Dictionary value - c#

I've list of object which looks like below
public class test
{
public int ID{ get; set; }
public string Name { get; set; }
public Dictionary<string, string> SampleXML { get; set; }
}
I want to group the list based on values in 'SampleXML'. e.g. It contains values like price. I want to group List based on price.
How to implement the same. I tried below code but it just seperates the list by key. I need unique records.
Dictionary<string, List<test>> result = Obj.LstTest
.GroupBy(x => x.SampleXML["Price"])
.ToDictionary(g => g.Key,
g => g.ToList());
The above code generates the list based on price. e.g. If there are 6 records with 3 different prices, above code return 3 results,but again the list contains two records each.
EDIT
If the price is blank then it needs to be ignored in grouping that is if there are 3 records and all doesn't have prices then list will contain 3 item(as it is).
Any help is appreciated.

Edit: Based on Vicky S edit
To still have the result as a List of test:
List<test> list = new List<test>();
List<test> result = new List<test>();
result = list.GroupBy(x => x.SampleXML["Price"]).Select(g=>g.FirstOrDefault());
Edit: To include tests that have SampleXML with empty Price value or no Price Key. Do the following:
First we need to separate tests with "No Price" and include them to our final result, from test that have "Price" value to group them.
var emptyPrice = list.Where(l => !l.SampleXML.ContainsKey("Price") || l.SampleXML["Price"] == string.Empty).ToList();
var withoutEmptyPrice = list.Where(l => l.SampleXML.ContainsKey("Price") && !string.IsNullOrEmpty(l.SampleXML["Price"]));
Then we will group tests with "Price" value as my first answer.
var resultWithEmptyPrice = withoutEmptyPrice.GroupBy(x => x.SampleXML["Price"]).Select(g => g.FirstOrDefault()).ToList();
Finally, we will add the "No Price" tests to our result.
resultWithEmptyPrice.AddRange(emptyPrice);

Related

How to group a list with Linq

I have a list which I get from a database. The structure looks like (which I'm representing with JSON as it's easier for me to visualise)
{id:1
value:"a"
},
{id:1
value:"b"
},
{id:1
value:"c"
},
{id:2
value:"t"
}
As you can see, I have 2 unique ID's, ID 1 and 2. I want to group by the ID. The end result I'd like is
{id:1,
values:["a","b","c"],
},
{id:2,
values["g"]
}
Is this possible with Linq? At the moment, I have a massive complex foreach, which first sorts the list (by ID) and then detects if it's already been added etc but this monstrous loop made me realise I'm doing wrong and honestly, it's too embarrassing to share.
You can group by the item Id and have the resulting type be a Dictionary<int, List<string>>
var result = myList.GroupBy(item => item.Id)
.ToDictionary(item => item.Key,
item => item.Select(i => i.Value).ToList());
You can either use GroupBy method on IEnumerable to create IGrouping object that contains a key and grouped objects or you can use ToLookupto create exactly what you want in result:
yourList.ToLookup(m => m.id, m => m.value);
This creates a hashed collection of keys with their values.
For more information please see below post:
https://www.c-sharpcorner.com/UploadFile/d3e4b1/practical-usage-of-using-tolookup-method-in-linq-C-Sharp/
Just a little more detail to emphasize the difference between the ToLookup approach and the GroupBy approach:
// class definition
public class Item
{
public long Id { get; set; }
public string Value { get; set; }
}
// create your list
var items = new List<Item>
{
new Item{Id = 0, Value = "value0a"},
new Item{Id = 0, Value = "value0b"},
new Item{Id = 1, Value = "value1"}
};
// this approach results in a List<string> (a collection of the values)
var lookup = items.ToLookup(i => i.Id, i => i.Value);
var groupOfValues = lookup[0].ToList();
// this approach results in a List<Item> (a collection of the objects)
var itemsGroupedById = items.GroupBy(i => i.Id).ToList();
var groupOfItems = itemsGroupedById[0].ToList();
So, if you want to work with values only after grouping, then you could take the first approach; if you want to work with objects after grouping, you could take the second approach. And, these are just a couple example implementations, there are plenty of ways to accomplish your goal.
First convert to a Lookup then select into a list, like so:
var groups = list
.ToLookup
(
item => item.ID,
item => item.Value
)
.Select
(
item => new
{
ID = item.Key,
Values = item.ToList()
}
)
.ToList();
The resulting JSON looks like this:
[{"ID":1,"Values":["a","b","c"]},{"ID":2,"Values":["t"]}]
Link to working example on DotNetFiddle.

Get the Count and Name

I have a class called Cars which has two properties i.e. Count and Name.
public class Cars
{
public string Name { get; set; }
public int Count{ get; set; }
}
I am trying to unit test my repository layer and am not able to get the value of Count correctly.
Here is my unit test
public void GetCarStats()
{
var mockRepo = new VehicleRepository();
var result = mockRepo.GetCarStats(Guid.Parse("9F733662-FP4E-69DC-AX600-A4C250F9E166"));
Assert.NotEmpty(result);
Assert.Equal(1, result.Count);
var cars= result.Where(x => x.Count > 0).Select( v => v.Count);
Assert.Equal(6, cars);//This statement is failing
}
Could anyone help me?
This statement
var cars= result.Where(x=>x.Count>0).Select(v=>v.Count);
will give you an IEnumerable<int> and you are trying to compare it with a single number 6 , this should fail.
Depending on what you need, you can get the First element from your collection and compare it with 6 or you can use Sum to get the total number of count.
var cars= result.Where(x=>x.Count>0).Select(v=>v.Count).FirstOrDefault();
Assert.Equal(6,cars);//This statement is failing
If you are interested in total number of records returned against your condition count > 0 then use Count instead of Select(v=> v.Count) like:
var cars = result.Where(x => x.Count > 0).Count();
or
var cars = result.Count(x=> x.Count > 0);
You are comparing a list of int to and int. Try this for sum of count:
var cars= result.Where(x=>x.Count>0).Sum(v=>v.Count);
Assert.Equal(6,cars);
and this for amount of results that have more than 0 cars:
var cars= result.Where(x=>x.Count>0).Select(v=>v.Count);
Assert.Equal(6,cars.Count());
Not sure which one you are looking for. This assumes you have setup the data for your test initially, otherwise the list will be empty anyway.

Linq - complex query - list in a list

I have this class:
public class RecipeLine
{
public List<string> PossibleNames { get; set; }
public string Name { get; set; }
public int Index { get; set; }
}
I have a list of multiple RecipeLine objects. For example, one of them looks like this:
Name: apple
PossibleNames: {red delicious, yellow delicious, ... }
Index = 3
I also have a table in my db which is called tblFruit and has 2 columns: name and id. the id isn't the same as the index in the class.
What I want to do is this:
for the whole list of RecipeLine objects, find all the records in tblFruit whose name is in PossibleNames, and give me back the index of the class and the id in the table. So we have a list in a list (a list of RecipeLine objects who have a list of strings). How can I do this with Linq in c#?
I'm pretty sure there isn't going to be a LINQ statement that you can construct for this that will create a SQL query to get the data exactly how you want. Assuming tblFruit doesn't have too much data, pull down the whole table and process it in memory with something like...
var result = tblFruitList.Select((f) => new {Id = f.id, Index = recipeLineList.Where((r) => r.PossibleNames.Contains(f.name)).Select((r) => r.Index).FirstOrDefault()});
Keeping in mind that Index will be 0 if there isn't a recipeLine with the tblFruit's name in it's PossibleNames list.
A more readable method that doesn't one-line it into a nasty linq statement is...
Class ResultItem {
int Index {get;set;}
int Id {get;set;}
}
IEnumerable<ResultItem> GetRecipeFruitList(IEnumerable<FruitItem> tblFruitList, IEnumerable<RecipeLine> recipeLineList) {
var result = new List<ResultItem>();
foreach (FruitItem fruitItem in tblFruitList) {
var match = recipeLineList.FirstOrDefault((r) => r.PossibleNames.Contains(fruitItem.Name));
if (match != null) {
result.Add(new ResultItem() {Index = match.Index, Id = fruitItem.Id});
}
}
return result;
}
If tblFruit has a lot of data you can try and pull down only those items that have a name in the RecipeLine list's of PossibleName lists with something like...
var allNames = recipeLineList.SelectMany((r) => r.PossibleNames).Distinct();
var tblFruitList = DbContext.tblFruit.Where((f) => allNames.Contains(f.Name));
To get all the fruits within your table whose Name is in PossibleNames use the following:
var query = myData.Where(x => myRecipeLines.SelectMany(y => y.PossibleNames).Contains(x.Name));
I don't think you can do this in a single step.
I would first create a map of the possible names to indexes:
var possibleNameToIndexMap = recipes
.SelectMany(r => r.PossibleNames.Select(possibleName => new { Index = r.Index, PossbileName = possibleName }))
.ToDictionary(x => x.PossbileName, x => x.Index);
Then, I would retrieve the matching names from the table:
var matchingNamesFromTable = TblFruits
.Where(fruit => possibleNameToIndexMap.Keys.Contains(fruit.Name))
.Select(fruit => fruit.Name);
Then you can use the names retrieved from the tables as keys into your original map:
var result = matchingNamesFromTable
.Select(name => new { Name = name, Index = possibleNameToIndexMap[name]});
Not fancy, but it should be easy to read and maintain.

Sorting a list of objects with OrderByDescending

I have an object (KS), which holds ID and Title (which has a number as part of the Title).
All I'm trying to do is sort it into descending order. The object has:
ID Title
1 1 Outlook VPN
2 2 Outlook Access
3 4 Access VBA
4 3 Excel Automation
So when order by Title, it should read:
ID Title
3 4 Access VBA
4 3 Excel Automation
2 2 Outlook Access
1 1 Outlook VPN
The code I'm using to sort it is:
IEnumerable<KS> query = results.OrderByDescending(x => x.Title);
However, query still has the objects in the original order!
Is there something to do with having numbers at the start of Title that I'm missing?
EDIT
I've added the code from the controller for clarity:
[HttpPost]
// [ValidateAntiForgeryToken]
// id is a string of words eg: "outlook access vpn"
// I split the words and want to check the Title to see how many words appear
// Then sort by the most words found
public JsonResult Lookup(string id)
{
List<string> listOfSearch = id.Split(' ').ToList();
var results = db.KS.Where(x => listOfSearch.Any(item => x.Title.Contains(item)));
// search each result, and count how many of the search words in id are found
// then add the count to the start of Title
foreach (KS result in results)
{
result.KSId = 0;
foreach (string li in listOfSearch)
{
if (result.Title.ToLower().Contains(li.ToLower()))
{
result.KSId += 1;
}
}
result.Title = result.KSId.ToString() + " " + result.Title;
}
// sort the results based on the Title - which has number of words at the start
IEnumerable<KS> query = results.OrderByDescending(x => x.Title).ToList();
return Json(query, JsonRequestBehavior.AllowGet);
}
Here is a screenshot after query has been populated showing Titles in the order: 1, 2, 1, 1:
Model for the object if it helps is:
public class KS
{
public int KSId { get; set; }
public string KSSol { get; set; }
public string Title { get; set; }
public string Fix { get; set; }
}
As I said in a comment, put a .ToList() where you declare your results variable. That is:
var results = db.KS.Where(x => listOfSearch.Any(item => x.Title.Contains(item)))
.ToList();
If you don't do that, the foreach loop will modify objects that might not be the same as the objects you sort later, because the database query is run again each time you enumerate your IQueryable<>.
You can always just ignore the strange behavior and go the safe way:
List<KS> query = results.ToList();
query.Sort((a, b) => a.Whatever.CompareTo(b.Whatever));
return Json(query, blah);
I simple did this and it worked for me :-
var sortedOrder = Query.OrderBy(b => b.Title.Substring(b.Title.IndexOf(" ")));
All I have done is SubString the Title at the index of of the blank space when ordering the objects in the sequence, that way, the OrderBy is looking at the first character in the title rather than the number at the beginning.
Old question, but maybe this will help someone using C#. I used the following expressions to sort a list of objects based on their quantity parameter in ascending or descending order. Can modify it to compare text as the original question was concerned with.
Ascending Order:
locationMaterials.Sort((x, y) => x.Quantity.CompareTo(y.Quantity));
Descending Order:
locationMaterials.Sort((x, y) => y.Quantity.CompareTo(x.Quantity));
You are missing .ToList()
IEnumerable<KS> query = results.OrderByDescending(x => x.Title).ToList();
results.OrderByDescending(x => x.Title) is a query, and it has no data.
ToList() forces the query to be executed.
[EDIT]
My answer assumes that your results has acually not been materialized, and that that is the source of your problem.

Storing the list items into a variable after filtering by using linq

I got a list of items, want to filter the list based on column distinct value(i.e based on Level) and also after filtering need to get the count and store them as an int variable.
Can anyone please help me.
**List**
Public Class Totalitems
{
public string ItemName;
public string ItemId;
public string ItemGroup;
public int Level;
}
Id= "123asd";
List<Totalitems> l_items = this.getslist(Id);
/*How to filter based on distinct level */
/* var filteredItems = (
from p in l_items
select p.Level)
.Distinct(); */
**Finally:**
//Stores the elements contained in the List into a variable
int totalItemsafterFiltering = l_FilteredItems.Count;
You want to use GroupBy for this task:
var numberOfDifferentLevels = l_items.GroupBy(x => x.Level).Count();
GroupBy is especially useful, if you want to do something with the actual elements in the group. For example, you might want to know how many items per level there are:
var itemsPerLevel = l_items.GroupBy(x => x.Level)
.Select(x => new { Level = x.Key,
NumberOfItems = x.Count() });
Another approach when you really only care about the number of distinct levels, is the following:
var numberOfDifferentLevels = l_items.Select(x => x.Level).Distinct().Count();

Categories