Using a semi-complex structure, I am trying to 'combine' several objects into one using the Linq Aggregate method (though if there is a better way, I am open to ideas).
Here is my basic class design.
class Aspect {
string Name { get; set; }
}
class Arrangement {
Aspect Aspect { get; set; }
IList<Aperture> Apertures { get; set; }
IList<Step> Steps { get; set; }
}
class Step {
int Rank { get; set; }
int Required { get; set; }
}
class Aperture {
string Name { get; set; }
int Size { get; set; }
}
Basically, I am trying to aggregate the entire hierarchy of an IEnumerable<Arrangement> and keep everything on the base level, but where things can appropriately overwrite, I want to overwrite them.
Update
I want to get all Arrangements that share the same Aspect.Name, and get a complete list of Steps, overwriting lower level Steps where higher level Arrangements have the same Rank with a different Required value.
So take for instance...
var list = new List<Arrangement>{
new Arrangement{
Aspect = Aspects.Named("One"),
Steps = new List<Step>{
new Step {
Rank = 1,
Required = 2
},
new Step {
Rank = 2,
Required = 4
}
}
},
new Arrangement{
Aspect = Aspects.Named("One"),
Steps = new List<Step>{
new Step {
Rank = 1,
Required = 3
}
}
}
}
When aggregated properly, it should come out to look like ...
Arrangement
- Aspect
- Name : One
- Steps
- Rank : 1
- Required : 3
- Rank : 2
- Required : 4
I have attempted to use Distinct and Aggregate and it just isn't getting me anywhere. I keep ending up not getting one list or the other. Can anyone help with this?
Update
Here is an example of my current aggregation.
public static Layouts.Template Aggregate(this IList<Layouts.Template> source) {
return source.Aggregate(
source.First(),
(current, next) => new Layouts.Template {
Apertures = (current.Apertures.Concat(next.Apertures).Distinct().ToList()),
Arrangements = (current.Arrangements.Concat(next.Arrangements).Distinct().ToList()),
Pages = (current.Pages.Concat(next.Pages).Distinct().ToList())
});
}
My problem is that I'm having a lot of trouble wrapping my head around how to do this at all, much less in one expression. I'm not unwilling to use multiple methods, but if I could encapsulate it all, it would be really useful. I am fascinated by LINQ in general and I really want to get my head around this.
Update 2
The other collection, Apertures, will work in a similar manner, but it is unrelated to the Steps. Simply two different arrays I must do the same thing to, but they have nothing in common with one another. Learning how to do this with one will give me the knowledge to do it with the other.
If there's no correlation between steps and apertures you can do this:
var result = new Arrangement{
Steps = list.SelectMany(arrangement => arrangment.Steps)
.GroupBy(step => step.Rank)
.Select(l => l.Last())
.OrderBy(step => step.Rank)
.ToList()),
}
If there is you'll need to combine the two somehow. If steps index into apertures then you can use something similar.
After your updates:
var query = arrangements
.GroupBy(a => a.Aspect.Name)
.Select(g =>
new Arrangement
{
Steps = ga.SelectMany(a => a.Steps)
.GroupBy(s => s.Rank)
.Select(gs => gs.Last()),
Aspect = ga.First().Aspect
});
This will create output as in your example.
Now, how to merge it with your current aggregation method? As to my understanging, you want to create one big layout from all current layout contents (including arrangements, pages, etc)?
You don't need aggregate at all, just split it into 3 LINQ queries:
// get all arrangements object from all layouts [flattening with SelectMany]
var arrangements = source.SelectMany(s => s.Arrangements);
// and filter them
var filteredArrangements = // enter larger query from above here
// repeat the same for Apertures and Pages
...
// and return single object
return new Layouts.Template
{
Apertures = filteredApertures,
Arrangements = filteredArrangements,
Pages = filteredPages
};
I assume this isn't the complete solution that you want:
private static Arrangement Accumulate(IEnumerable<Arrangement> arrangements)
{
var stepsByRank = new Dictionary<int, Step>();
foreach (var arrn in arrangements)
foreach (var step in arrn.Steps)
stepsByRank[step.Rank] = step;
return new Arrangement { Steps = stepsByRank.Values.ToArray() };
}
What's missing from this? How else do you want to "aggregate" the arrangements and steps? (edit: after reading your comment, maybe this actually is what you want.)
Related
I want to sort a List Array on the basis of an array item.
I have a List Array of Strings as below:
List<String>[] MyProjects = new List<String>[20];
Through a loop, I have added five strings
(Id, Name, StartDate, EndDate, Status)
to each of the 20 projects from another detailed List source.
for(int i = 0; i<20; i++){
MyProjects[i].Add(DetailedProjectList.Id.ToString());
MyProjects[i].Add(DetailedProjectList.Name);
MyProjects[i].Add(DetailedProjectList.StartDate);
MyProjects[i].Add(DetailedProjectList.EndDate);
MyProjects[i].Add(DetailedProjectList.Status)}
The Status values are
"Slow", "Normal", "Fast", "Suspended" and "" for unknown status.
Based on Status, I want to sort MyProject List Array.
What I have done is that I have created another List as below
List<string> sortProjectsBy = new List<string>(){"Slow", "Normal", "Fast", "", "Suspended"};
I tried as below to sort, however unsuccessful.
MyProjects = MyProjects.OrderBy(x => sortProjectsBy.IndexOf(4));
Can anyone hint in the right direction. Thanks.
I suggest you to create class Project and then add all the fields inside it you need. It's much nicer and scalable in the future. Then create a List or an Array of projects and use the OrderBy() function to sort based on the field you want.
List<Project> projects = new List<>();
// Fill the list...
projects.OrderBy(project => project.Status);
The field Status has to be a primitive type or needs to implement the interface IComparable in order for the sorting to work. I suggest you add an enum for Status with int values.
First consider maybe to use Enum for status and put it in a different file lite (utils or something) - better to work like that.
enum Status {"Slow"=1, "Normal", "Fast", "", "Suspend"}
Now about the filtering you want to achieve do it like this (you need to tell which attribute of x you are referring to. In this case is status)
MyProjects = MyProjects.OrderBy(x => x.status == enum.Suspend);
Read about enums :
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/enum
Read about lambda expressions :
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions
First of all, storing project details as List is not adivisable. You need to create a Custom Class to represent them.
For example,
public class DetailedProjectList
{
public string Name {get;set;}
public eStatus Status {get;set;}
// rest of properties
}
Then You can use
var result = MyProjects.OrderBy(x=> sortProjectsBy.IndexOf(x.Status));
For example
List<string> sortProjectsBy = new List<string>(){"Slow", "Normal", "Fast", "", "Suspended"};
var MyProjects= new List<DetailedProjectList>{
new DetailedProjectList{Name="abc1", Status="Fast"},
new DetailedProjectList{Name="abc2", Status="Normal"},
new DetailedProjectList{Name="abc3", Status="Slow"},
};
var result = MyProjects.OrderBy(x=> sortProjectsBy.IndexOf(x.Status));
Output
abc3 Slow
abc2 Normal
abc1 Fast
A better approach thought would be to use Enum to represent Status.
public enum eStatus
{
Slow,
Normal,
Fast,
Unknown,
Suspended
}
Then your code can be simplified as
var MyProjects= new List<DetailedProjectList>{
new DetailedProjectList{Name="abc1", Status=eStatus.Fast},
new DetailedProjectList{Name="abc2", Status=eStatus.Normal},
new DetailedProjectList{Name="abc3", Status=eStatus.Slow},
};
var result = MyProjects.OrderBy(x=> x.Status);
Ok so you have a collection of 20 items. Based on them you need to create a list of strings(20 DetailedProjectList items).
What you can do to solve your problem is to SORT YOUR COLLECTION before you create your list of strings. In this way your list of strings will be sorted.
But your code is not optimal at all. So you should concider optimization on many levels.
Lets say you have ProjectDetail class as follow:
private class ProjectDetail
{
public int Id {get;set;}
public string Name {get;set;}
DateTime StartDate {get;set;} = DateTime.Now;
DateTime EndDate {get;set;} = DateTime.Now;
public string Status {get;set;}
public string toString => $"{Id} - {Name} - {StartDate} - {EndDate} - {Status}";
}
Notice that I have added a toString attribute to make things easier, and I also have added default values.
Then your program could be like:
static void Main(string[] args)
{
var projectDetails = MockProjectItems();
Console.WriteLine("Before sortig:");
foreach (var item in projectDetails)
{
Console.WriteLine(item.toString);
}
var myProjects = projectDetails.OrderBy(p => p.Status).Select(p => p.toString);
Console.WriteLine("\n\nAfter sorting:");
foreach (var item in myProjects)
{
Console.WriteLine(item);
}
}
where the helper method is
private static List<ProjectDetail> MockProjectItems()
{
var items = new List<ProjectDetail>(20);
for(int i = 0; i < 20 ; i += 4){
items.Add(new ProjectDetail{Id = i, Name = "RandomName "+i, Status = "Slow"});
items.Add(new ProjectDetail{Id = i+1, Name = "RandomName "+(i+1), Status = "Normal"});
items.Add(new ProjectDetail{Id = i+2, Name = "RandomName "+(i+2), Status = "Fast"});
items.Add(new ProjectDetail{Id = i+3, Name = "RandomName "+(i+3), Status = "Suspended"});
}
return items;
}
Then your program should print the following:
i know it is not complicated but i struggle with it.
I have IList<Material> collection
public class Material
{
public string Number { get; set; }
public decimal? Value { get; set; }
}
materials = new List<Material>();
materials.Add(new Material { Number = 111 });
materials.Add(new Material { Number = 222 });
And i have DbSet<Material> collection
with columns Number and ValueColumn
I need to update IList<Material> Value property based on DbSet<Material> collection but with following conditions
Only one query request into database
The returned data from database has to be limited by Number identifier (do not load whole database table into memory)
I tried following (based on my previous question)
Working solution 1, but download whole table into memory (monitored in sql server profiler).
var result = (
from db_m in db.Material
join m in model.Materials
on db_m.Number.ToString() equals m.Number
select new
{
db_m.Number,
db_m.Value
}
).ToList();
model.Materials.ToList().ForEach(m => m.Value= result.SingleOrDefault(db_m => db_m.Number.ToString() == m.Number).Value);
Working solution 2, but it execute query for each item in the collection.
model.Materials.ToList().ForEach(m => m.Value= db.Material.FirstOrDefault(db_m => db_m.Number.ToString() == m.Number).Value);
Incompletely solution, where i tried to use contains method
// I am trying to get new filtered collection from database, which i will iterate after.
var result = db.Material
.Where(x=>
// here is the reasonable error: cannot convert int into Material class, but i do not know how to solve this.
model.Materials.Contains(x.Number)
)
.Select(material => new Material { Number = material.Number.ToString(), Value = material.Value});
Any idea ? For me it is much easier to execute stored procedure with comma separated id values as a parameter and get the data directly, but i want to master linq too.
I'd do something like this without trying to get too cute :
var numbersToFilterby = model.Materials.Select(m => m.Number).ToArray();
...
var result = from db_m in db.Material where numbersToFilterBy.Contains(db_m.Number) select new { ... }
I am new to databases, and to EF. I am using EF within an ASP.NET Core MVC project. The implementation code below is from a Controller, aiming to combine data from two tables into a summary.
The database has tables: Batch, Doc.
Batch has many columns, including: int BatchId, string BatchEnd. BatchEnd is a consistently formatted DateTime, e.g. 23/09/2016 14:33:21
Doc has many columns including: string BatchId, string HardCopyDestination. Many Docs can refer to the same BatchId, but all Docs that do so have the same value for HardCopyDestination.
I want to populate the following ViewModel
public class Batch
{
public int BatchId { get; set; }
public string Time { get; set; } // from BatchEnd
public string HardCopyDestination { get; set; }
}
But my current query, below, is running dog slow. Have I implemented this correctly?
var BatchViewModels = new List<Batch>();
// this is fine
var batches = _context.BatchTable.Where(
b => b.BatchEnd.Contains(
DateTime.Now.Date.ToString("dd/MM/yyyy")));
// this bit disappears down a hole
foreach (var batch in batches)
{
var doc = _context.DocTable.FirstOrDefault(
d => d.BatchId == batch.BatchId.ToString());
if (doc != null)
{
var newBatchVM = new Batch
{
BatchId = batch.BatchId,
Time = batch.BatchEnd.Substring(whatever to get time),
HardCopyDestination = doc.HardCopyDestination
};
BatchViewModels.Add(newBatchVM);
continue;
}
}
return View(BatchViewModels);
I think you're hitting the database once per batch. If you have many batches that is expensive. You can get all documents in one go from db.
var batchDict = batches.ToDictionary(b => b.BatchId);
var documents = _context.DocTable.Where(doc => batchDict.Keys.Contains(doc.BatchId));
BatchViewModels.AddRange(documents.Select(d => new Batch
{
BatchId = d.BatchId,
Time = batchDict[d.BatchId].BatchEnd.TimeOfDay, // you only want the time?
HardCopyDestination = d.HardCopyDestination
});
By the way, Igor is right about dates and in addition, if BatchId is int in BatchTable, then it should be that in DocTable as well. In above code I assume they are same type but shouldn't be so hard to change if they aren't.
Igor is also right about profiling db is a good way to see what the problem is. I'm just taking a guess based on your code.
Apologies I haven't done very well with the title of the question, hopefully it will be apparenent with some code
I've created a class that stores poker hand information as follows
public class BestHandSummary<T>
{
public Func<T, bool> Test { get; set; }
public Ranking Rank { get; set; } //Enum: Nothing, TwoPair, Straight, Flush, Poker :in ascending order
public int BestCard { get; set; }
public BestHand(Ranking r)
{
Rank = r;
}
..//default ctor
}
I initialised the rules collection in order of most valuable hand so that when I take the First() of the matched rules the most powerful hand will be chosen as best hand.
rules = new List<BestHandSummary<PokerHand>>()
{
new BestHandSummary<PokerHand> { Test = h => h.HandContainsFourOfAKind(out bestCard),
Rank = Ranking.FourOfAKind,
BestCard = bestCard },
new BestHandSummary<PokerHand> { Test = h => h.HandContainsFlush(),
Rank = Ranking.Flush },
new BestHandSummary<PokerHand> { Test = h => h.HandContainsStraight(out bestCard),
Rank = Ranking.Straight,
BestCard = bestCard },
new BestHandSummary<PokerHand> { Test = h => h.HandContainsTwoPair(out bestCard),
Rank = Ranking.Straight,
BestCard = bestCard },
};
private BestHandSummary<PokerHand> GetAPlayersBestHand(PokerHand hand)
{
bool hasAny = rules.Any(r => r.Test(hand));
if (hasAny)
{
return rules.Where(r => r.Test(hand) == true).First();
}
return new BestHandSummary<PokerHand>(Ranking.Nothing);
}
What I can't seem to figure out is how can I tunnel the out param bestCard into the BestCard property of the BestHandSummary class? The code above doesn't work, BestCard = bestCard doestn't get assigned, which I can understand why, but I'm wondering if there is any small change I can make to fix it..
int bestCard;
new BestHand<PokerHand> { Test = h => h.HandContainsFourOfAKind(out bestCard),
Rank = Ranking.FourOfAKind,
BestCard = bestCard },
This code won't work, because HandContainsFourOfAKind was never called, and so nothing was assigned to bestCard.
I'm only interested in bestCard when the rule matched. It is used for when there is a draw between two players. E.G. H= 22KJJ -> best card is Jack, not king
So, you want BestCard to be assigned only when Test is invoked? This will do.
var hand = new BestHand<PokerHand> {Rank = Ranking.FourOfAKind};
hand.Test = h =>
{
int bestCard; //var local to the lambda's scope
bool contains = h.HandContainsFourOfAKind(out bestCard);
hand.BestCard = bestCard;
return contains;
};
I've written an equalizer myself a few years ago. I don't have a direct answer to your question, but one thing comes to my mind:
In your example, if you just look at the best card involved building the rank like the Jack in the two-pair hand, you won't figure out the best hand, if both have the same two-pair with different kicker. You might have to use a third method on that.
Maybe you are better of to do this in one piece. I solved this with an if-monster, which returned an exact long-value, indicating every card in it's value for that hand.
Is there a convenient way to remove a nested list from another list if it meets certain requirements? For example, say we have a collection of stops, and we decide to call each collection of stops a route. Each route is in list from. Then we decide to put each route into a list as well.
So now that we have a list of routes, someone decides that certain types of routes really shouldn't be included in the route list. How can I remove those routes? Here's some sample code:
Example Class
public class Stops
{
public Stops(int _param1, string _param2)
{
param1 = _param1;
param2 = _param2;
}
public int param1 { get; set; }
public string param2 { get; set; }
}
Create the Lists
List<List<Stops>> lstRoutes = new List<List<Stops>>();
List<Stops> lstStops = new List<Stops>();
List<Stops> lstMoreStops = new List<Stops>();
// Create some stops
for (int i = 0; i < 5; i++)
{
lstStops.Add(new Stops(i, "some text"));
}
lstRoutes.Add(lstStops);
// Create some more stops
for (int i = 5; i < 10; i++)
{
lstMoreStops.Add(new Stops(i, "some more text"));
}
lstRoutes.Add(lstMoreStops);
How can I remove any route from lstRoutes that has, say, any param1 value greater than 6?
The simplest way (which can be applicable to all enumerables, not just lists) would be:
lstRoutes = lstRoutes.Where(r => !r.Any(s => s.param1 > 6)).ToList();
The snippet above creates a new list, so copying will occur which means both the performance and memory usage will slightly suffer. The most efficient way would be not adding those items to the list in the first place.
The second most efficient way would be to remove items from the list instead of constructing a new one, so the memory usage wouldn't be affected as much:
lstRoutes.RemoveAll(r => r.Any(s => s.param1 > 6));
List<Stops> stop = lstRoutes.Find(delegate(List<Stops> stp) { return stp.param1 > 6; });