This question already has answers here:
Flatten a list which one of its properties is another list of object
(3 answers)
Closed 1 year ago.
Using Linq what will be the best way of making a flattened list with these 2 objects lists
public class DataDto
{
public string StudentID { get; set; }
public List<ScoreDto> Scores { get; set; }
}
public class ScoreDto
{
public string ExamID { get; set; }
public double Mark { get; set; }
}
into a list that will present as
public class FinalDto
{
public string StudentID { get; set; }
public string ExamID { get; set; }
public double Mark { get; set; }
}
With StudentID repetitions for the number of ScoreDtos present for a particular student.
Example data -
var data = new List<DataDto>()
{
new DataDto
{
StudentID = "S1",
Scores = new List<ScoreDto>()
{
new ScoreDto { ExamID = "01", Mark = 5},
new ScoreDto { ExamID = "02", Mark = 15},
new ScoreDto { ExamID = "03", Mark = 25}
}
},
new DataDto
{
StudentID = "S2",
Scores = new List<ScoreDto>()
{
new ScoreDto { ExamID = "01", Mark = 1},
new ScoreDto { ExamID = "02", Mark = 5},
new ScoreDto { ExamID = "03", Mark = 20}
}
}
};
So it produces -
StudentID, ExamID, Mark
S1, 01, 5
S1, 02, 15
S1, 03, 25
S2, 01, 1
S2, 02, 5
S2, 03, 20
You use SelectMany to flatten a list of lists:
var final = data.SelectMany(
student => student.Scores,
(student, score) => new FinalDto {
StudentID = student.StudentID,
ExamID = score.ExamID,
Mark = score.Mark
}
);
Console.WriteLine("StudentID, ExamID, Mark");
foreach (var result in final)
{
Console.WriteLine("{0}, {1}, {2}", result.StudentID, result.ExamID, result.Mark);
}
Alternatively, you can use a different overload of SelectMany along with a nested Select projection:
var final = data.SelectMany(
student => student.Scores.Select(
score => new FinalDto {
StudentID = student.StudentID,
ExamID = score.ExamID,
Mark = score.Mark
}
)
);
You can also use query syntax:
var final = (
from student in data
from score in student.Scores
select new FinalDto {
StudentID = student.StudentID,
ExamID = score.ExamID,
Mark = score.Mark
}
);
Note that this is just translated to the second form shown above.
There's no "best" here. Forms two and three literally compile to the same exact code. Form one is just a slight variation. There is no impact (speed, memory) for choosing one over the other. Pick the one that is visually appealing to you. Personally I don't write query syntax (the last form) and I always forget the first overload exists so I end up using the second one.
var result = (
from item in data
from score in item.Scores
select new FinalDto {
StudentID = item.StudentID,
ExamID = score.ExamID,
Mark = score.Mark
}).ToList();
You have two tables with a one-to-many relation: every item of table A has zero or more items from table B, and every item from table B belongs to exactly one item of table A, namely the one item that the foreign key refers to.
Whenever you have this, and you want "items with their sub-items", like Schools with their Students, Customers with their Orders and Authors with their Books, consider to use one of the overloads of Queryable.GroupJoin
If you don't want the items with their sub-items, but a flat table,consider to use Queryable.Join
Apparently you already have fetched the "Students with their zero or more Scores", and you want a flat result. In that case you use SelectMany
IEnumerable<DataDto> studentsWithTheirScores = ...
var result = studentsWithTheirScores.SelectMany(
// parameter collectionSelector: where are the sub-items stored?
student => student.Scores,
// parameter resultSelector: take each student, and each one of his Scores
// to make one new
(student, score) => new FinalDto
{
StudentId = student.StudentId,
ExamId = score.ExamId,
Mark = score.Mark,
});
Related
I have an object:
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
I return a list that may look like the following:
var students = new List<Student>() {
new Student(){ Id = 1, Name="Bill85"},
new Student(){ Id = 2, Name="Bill66"},
new Student(){ Id = 3, Name="Ram7895"},
new Student(){ Id = 4, Name="Ram5566"},
new Student(){ Id = 5, Name="Join1230"}
};
I want to group them together if they have similar names, to get the result like below, is there any quick way to solve this?
GroupStudentList
UserList
Id = 1, Name="Bill85"
Id = 2, Name="Bill66"
UserList
Id = 3, Name="Ram7895"
Id = 4, Name="Ram5566"
UserList
Id = 3, Name="Join1230"
public class Student
{
public List<Student> ListGroupStudent {get; set;}
//add one list property...
}
var listGroup = listStudent.GroupBy(
x => x.Name,
(key, y) => new { ListGroupStudent = y.ToList() });
Making the assumption that similar names refers to identical names when all digits are removed from each name (as suggested by #klaus-gütter), a simple implementation could be the following:
Group the students based on their Name, but stripped for digits (using Regex)
For each group of similarily named students, select the students in that group to constitute a sublist
resulting in a nested List of Students:
List<List<Student>> groupStudentList = students
.GroupBy(
s => Regex.Replace(s.Name, "[0-9]*", string.Empty),
(similarName, students) => students.ToList())
.ToList();
The Regex expression above basically says "Replace all (if any) digits between 0 and 9 that exist in s.Name with an empty string". This means that if you have two students named "Tom1" and "T3o4m", they will also be grouped together -- because both names stripped for digits will be "Tom".
Optionally, you could create a UserList class:
public class UserList
{
public List<Student> Students { get; set; }
}
and create a UserList object for each grouping of students based on their name similarity:
List<UserList> groupStudentList = students
.GroupBy(
s => Regex.Replace(s.Name, "[0-9]*", string.Empty),
(similarName, students) => new UserList { Students = students.ToList() })
.ToList();
I have class that is used to get and send data to database. Data example:
Id
Amount
ProductName
RecordDateTime
1
2
Fresh Apples
23/2/2021
2
3
Sweet Bananas
13/6/2021
3
1
Yellow Bananas
12/7/2021
4
7
Green Apples
31/5/2021
5
9
Juicy Apples
12/9/2021
6
4
Young Potato's
5/2/2021
7
5
Orange Carrots
4/6/2021
Class:
public class LogModel
{
public int Id { get; set; }
public int Amount { get; set; }
public string ProductName { get; set; }
public DateTime RecordDateTime { get; set; }
}
Then I have another class that is used for user customization so that the user can build their own category structure. This is treeview with user-built structure like:
- Categories
- Fruits
- Apples | Bananas
- Vegetables
- Potato's | Carrots
Here is the class:
public class CategoryModel : BaseViewModel
{
private ObservableCollection<CategoryModel> categoryItems;
public ObservableCollection<CategoryModel> CategoryItems
{
get => this.categoryItems;
set
{
this.categoryItems = value;
this.OnPropertyChanged();
}
}
private string itemName;
public string ItemName
{
get => this.itemName;
set
{
this.itemName = value;
this.OnPropertyChanged();
}
}
}
I need to create a new list that will assign each record from the database to own category based on string.Contains after splitting by |. I have managed to get method with Linq to sort only needed data to work with from BD. How to do actual assign of data to each category? No need to match my structure. Basic linq example would be enough.
public List<LogModel> GetCategorySplit(DateTime startDate, DateTime endDate)
{
using (var db = new SQLDBContext.SQLDBContext())
{
List<LogModel> result = db.LogModel
.Where(w => w.RecordDateTime >= startDate.Date && w.RecordDateTime.Date <= endDate.Date && w.Amount != 0)
.Select(
s => new LogModel
{
ProductName = s.ProductName,
Amount = s.Amount,
})
.ToList();
// I have tried some foreach loop after that, but it is not so clear in my head how this process
// should go overall
//foreach (var item in result)
//{
// if (ExtensionMethods.StringContains(item.ProductName, value))
// {
// }
//}
return result;
}
}
Expected Output:
Fruits 22
Vegetables 9
P.S. Please comment if something is unclear, I am quite new here.
In order to be able to test my solution I've used in-memory data structures.
var dataSource = new List<LogModel>
{
new LogModel { Id = 1, Amount = 2, ProductName = "Fresh Apples", RecordDateTime = new DateTime(2021, 02, 23)},
new LogModel { Id = 2, Amount = 3, ProductName = "Sweet Bananas", RecordDateTime = new DateTime(2021, 06, 13)},
new LogModel { Id = 3, Amount = 1, ProductName = "Yellow Bananas", RecordDateTime = new DateTime(2021, 07, 12)},
new LogModel { Id = 4, Amount = 7, ProductName = "Green Apples", RecordDateTime = new DateTime(2021, 05, 31)},
new LogModel { Id = 5, Amount = 9, ProductName = "Juicy Apples", RecordDateTime = new DateTime(2021, 09, 12)},
new LogModel { Id = 6, Amount = 4, ProductName = "Young Potato's", RecordDateTime = new DateTime(2021, 02, 05)},
new LogModel { Id = 7, Amount = 5, ProductName = "Orange Carrots", RecordDateTime = new DateTime(2021, 06, 04)}
};
var categories = new List<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
};
The grouping logic can be written like this:
var topLevelQuantities = new Dictionary<string, int>();
foreach (var topLevelCategory in categories)
{
var filters = topLevelCategory.CategoryItems.Select(leafLevel => leafLevel.ItemName);
var count = dataSource.Where(log => filters.Any(filter => log.ProductName.Contains(filter)))
.Sum(log => log.Amount);
topLevelQuantities.Add(topLevelCategory.ItemName, count);
}
Here I iterate through the top level categories foreach (var topLevelCategory in categories)
I assumed the the graph depth is 2 (so there are top level and leaf level entities)
Then I gather the leafs' ItemNames into the filters
I perform a filtering based on the filters dataSource.Where( ... filters.Any(...))
And finally I calculate the Sum of the filtered LogModels' Amount
In order to be able to pass the accumulated data to another function/layer/whatever I've used a Dictionary<string, int>
I've used the following command to examine the result:
foreach (var (categoryName, categoryQuantity) in topLevelQuantities.Select(item => (item.Key, item.Value)))
{
Console.WriteLine($"{categoryName}: {categoryQuantity}");
}
NOTES
Please bear in mind that this solution was designed against in-memory data. So, after data has been fetched from the database. (If you want to perform this on the database level then that requires another type of solution.)
Please also bear in mind that this solution requires multiple iterations over the dataSource. So, if the dataSource is very large (or there are lots of top level categories) than the solution might not perform well.
UPDATE: When the depth of the category hierarchy is 3.
If we can assume that there can be only 1 top-level entity then we should not need to change too much:
var rootCategory = new CategoryModel
{
ItemName = "Categories",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
}
};
var midLevelQuantities = new Dictionary<string, int>();
foreach (var midLevelCategory in rootCategory.CategoryItems)
{
...
}
If there can be multiple top level categories:
var categories = new List<CategoryModel>
{
new CategoryModel
{
ItemName = "Categories",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
}
}
};
then we need to use recursive graph traversal.
I've introduced the following helper class to store the calculations' result:
public class Report
{
public string Name { get; }
public string Parent { get; }
public double Quantity { get; }
public Report(string name, string parent, double quantity)
{
Name = name;
Parent = parent;
Quantity = quantity;
}
}
The traversal can be implemented like this:
private static List<Report> GetReports(CategoryModel category, string parent, List<Report> summary)
{
if (category.CategoryItems == null || category.CategoryItems.Count == 0)
{
var count = dataSource.Where(log => log.ProductName.Contains(category.ItemName)).Sum(log => log.Amount);
summary.Add(new Report(category.ItemName, parent,count));
return summary;
}
foreach (var subCategory in category.CategoryItems)
{
summary = GetReports(subCategory, category.ItemName, summary);
}
var subTotal = summary.Where(s => s.Parent == category.ItemName).Sum(s => s.Quantity);
summary.Add(new Report(category.ItemName, parent, subTotal));
return summary;
}
In the if block we handle that case when we are at the leaf level
That's where we perform the queries
In the foreach block we iterate through all of its children and we are calling the same function recursively
After the foreach loop we calculate the current category's subTotal by aggregating its children's Quantities
Please note: This design assumes that the category names are unique.
To display the results we need yet another recursive function:
private static void DisplayResult(int depth, IEnumerable<CategoryModel> categories, List<Report> report)
{
foreach (var category in categories)
{
var indentation = new string('\t', depth);
var data =report.Single(r => r.Name == category.ItemName);
Console.WriteLine($"{indentation}{data.Name}: {data.Quantity}");
if (category.CategoryItems == null || category.CategoryItems.Count == 0)
continue;
DisplayResult((byte)(depth +1), category.CategoryItems, report);
}
}
Based on the category's level (depth) we calculate the indentation
We lookup the corresponding report based on the ItemName property of the category
We print out the report
If the current category does not have subcategories then we move to the next item
Otherwise we call the same function for the subcategories recursively
Let's put all things together
var report = new List<Report>();
foreach (var category in categories)
{
var subReport = GetReports(category, "-", new List<Report>());
report.AddRange(subReport);
}
DisplayResult(0, categories, report);
The output will be:
Categories: 31
Fruits: 22
Apples: 18
Bananas: 4
Vegetables: 9
Potato's: 4
Carrots: 5
So you have a displayed sequence of CategoryModels, with ItemNames like "Fruits", "Vegetables", etc.
Every CategoryModel has some sub-CategoryModels in property CategoryItems. In your example, CategoryModel "Fruits" has CategoryItems with ItemNames like "Apples" and "Bananas". "Vegetables has sub category ItemNames like "Potato's" and "Carrots".
You forgot to tell us, can a CategoryItem with ItemName "Apples" also have sub-categories, so you have sub-sub-categories? And can these sub-sub-categories have more sub-sub-sub-categories?
I need to create a new list that will assign each LogModel from the database to a CategoryModel.
How do you decide which LogModels belongs to "Apples". Is it if LogModel.ProductName contains "Apples"?
Apparently you need an extend class LogModel with a method that says: "Yes, a Juicy Apple is an Apple", or "No, a Tasty Pomodoro is not an Apple", neither is a "Granny Smith"
By the way: do you see the flaw in your requirement: why is a "Granny Smith" not an apple, like a "Juicy Apple"?
But let's design for change: if you later want to change how you match a LogModel with a CategoryModel, changes will be small.
Luckily, you already fetched the LogModels that you want to process from the database, so we can work "AsEnumerable", instead of "AsQueryable". This gives us more freedom in the functions that we can use.
public static bool IsMatch(LogModel log, CateGoryModel category)
{
// TODO: compare log with category, and decide whether it is a match
}
Usage will be as if IsMatch is a method of LogModel:
LogModel log = ...
CategoryModel category = ...
bool isMatch = IsMatch(log, category);
Your current requirement seems to be that a log matches a category, if the ProductName of the log contains the ItemName of the category. So if "Juicy Apples" contains "Apples" (not sure if you want case sensitivity)
public static bool IsMatch(LogModel log, CateGoryModel category)
{
// TODO: decide what to do if log == null, category == null.
// TODO: decide what to do if log.ProductName == null or category.ItemName == null
return log.ProductName.Contains(category.ItemName, StringComparison.CurrentCulture);
}
If later you decide that you don't check on names, but for instance on a FruitType, than changes will only have to be in this method, nowhere else.
Why is fruits 22, and vegetables 9?
Well, fruits has apples and bananas, and the sum of all matching apples (according to the method defined above) and all matching bananas is 22. Similarly: vegetables is potato's and carrots and the sum of all matching potato's (4) and matching carrots (5) is 9
So for every Category, we take all SubCategories, and find the LogModels that match. We sum all Amounts of the matching LogModels.
So as a matter of fact, you would like to extend class CategoryModel with a method that takes all fetched LogModels and returns the Amount of items. Something like this:
class CategoryModel
{
...
public int GetAmount(IEnumerable<LogModel> logModels)
{
...
}
}
If you don't want or if you cannot add this method to CategoryModel, you can always create an extension method. For those who are not familiar with extension methods, see Extension Methods Demystified
public static int GetAmount(this CategoryModel category, IEnumerable<LogModel> logModels)
{
// TODO: exception if items null or empty
int amount = logModels.Where(log => IsMatch(log, category)
.Select(log => log.Amount)
.Sum();
// Add all amounts of the subCategories:
if (category.CategoryItems != null && category.CategoryItems.Count != 0)
{
amount += category.CategoryItems
.Select(catgoryItem => categoryItem.GetAmounts(logModels))
.Sum();
}
return amount;
}
Nota bene: this method uses recursion, so it works even if you have sub-sub-sub-... categories.
Usage:
var fetchedLogModels = db.LogModels.Where(...).Select(...).ToList();
IEnumerable<CategoryModel> categories = ...
categoryies are "fruits" and "vegetables" etc.
var result = categories.Select(category => new
{
CategoryName = category.ItemName,
Amount = category.GetAmounts(fetchedLogModels),
});
Well, doesn't that seem like some nice piece of code? Every difficulty is hidden somewhere deep inside and the code is easily changeable and unit testable. If you want to change how you make your Matches, or want to change that GetAmounts is not recursive anymore, or that it becomes a method of CategoryModels: none of the users have to change.
I'm using EF 6 Code-First and I want my costumers to introduce new records in multiple languages. To store this I save all the texts in a Dictionary entity with a custom key and reference it from my other entity through this key.
This is a basic sample of my entities:
Dictionary
ID Key IsoCode Value
1 firstEntry en-US My new entry
2 secondEntry en-US My second entry
3 firstEntry es-ES Mi nueva entrada
Entries
ID Name CreationDate
1 firstEntry 2020-11-04
2 secondEntry 2020-11-07
What I want to achieve is to query the Entries entity with an ISO Code, and get a new Entity where the Name field is replaced with Value field from the Dictionary entity.
This is what I have right now:
public List<Entry> GetEntries(string isoCode)
{
var query = (from e in dbContext.Entry
join d in dbContext.Dictionary on e.Name equals d.Key
where d.IsoCode == isoCode
select new
{
entry= e,
Text = d.Value
}).ToList();
return query.Select(t => new Entry
{
Id = t.entry.Id,
Name = t.Text,
CreationDate = t.CreationDate
}).ToList();
}
Is there a better way to do this without creating two lists?
Is this approach of using a Key to get translated text a best practice or am I missing the point here?
Here's a working example of your code using 2 lists instead of your tables (of course).
You can simply create the Entry in the first query. You don't need the .ToList() in that routine. Just return the IEnumerable.
using System;
using System.Collections.Generic;
using System.Linq;
namespace JoinEntities
{
class Program
{
static List<Dict> dictionary = new List<Dict>
{
new Dict
{
ID = 1,
Key = "firstEntry",
IsoCode = "en-US",
Value = "My new entry"
},
new Dict
{
ID = 2,
Key = "secondEntry",
IsoCode = "en-US",
Value = "My second entry"
},
new Dict
{
ID = 3,
Key = "firstEntry",
IsoCode = "es-ES",
Value = "Mi nueva entrada"
},
};
static List<Entry> entries = new List<Entry>
{
new Entry
{
Id = 1,
Name = "firstEntry",
CreationDate = new DateTime(2020, 11, 04)
},
new Entry
{
Id = 1,
Name = "secondEntry",
CreationDate = new DateTime(2020, 11, 07)
}
};
static void Main(string[] args)
{
var list = GetEntries("en-US");
}
public static IEnumerable<Entry> GetEntries(string isoCode)
{
return from e in entries
join d in dictionary on e.Name equals d.Key
where d.IsoCode == isoCode
select new Entry
{
Id = e.Id,
Name = d.Value,
CreationDate = e.CreationDate
};
}
}
internal class Dict
{
public int ID { get; set; }
public string Key { get; set; }
public string IsoCode { get; set; }
public string Value { get; set; }
}
internal class Entry
{
public object Id { get; set; }
public object Name { get; set; }
public object CreationDate { get; set; }
}
}
Couldnt find exact question... I want to create a list(or dictionary or array, whatever it is called in C#, .NET), where i can store different types of arrays/lists/dicts.
for example, in PHP, i do in this way:
$x= array (
'Names'=> array( "James", "Nicolas", "Susan"), //Strings
'Age'=> array( 18, 52, 37), //Int
'Male'=> array( true, true, false), //Bool
);
How to achieve similar in C# / .NET ?
p.s. or if possible, Multi-Multi types, like:
$y = array (
$x => (
..... multi-element, like above
),
$z => (
..... multi-element, like above
)
);
In pre-version 7, C#, you have to create a class that can store your lists:
class Item {
public List<string> Names { get; }
public List<int> Ages { get; }
public List<bool> Males { get; }
}
You can also use a tuple, with the disadavantage of not having descriptive property names:
Tuple<List<string>, List<int>, List<bool>> tuple =
Tuple.Create(new List<string>(), new List<int>(), new List<bool>());
In C# 7 you can use value tuples without having to create a class:
(List<string> Names, List<int> Ages, List<bool> Males) itemLists =
(new List<string>(), new List<int>(), new List<bool>());
And access the components like this:
List<string> names = itemLists.Names;
You should though seriously consider to not create a class that contains lists, but a list that contains classes (or tuples). Here is an example with C# 7 value tuples:
List<(string Name, int Age, bool Male)> list = new List<(string, int, bool)>();
This construct is usually easier to handle, because you can loop one list and then handle one item that contains all related data.
Create a class that holds the information as List<string>, List<int> and List<string>. However a much better approach is to hold all the information for a single entity was a single class and store a list of those items:
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public bool Male { get; set; }
}
Now store instances of that type in a list:
var list = new List<Person> {
new Person { Name = "James", Age = 18, Male = true },
new Person { Name = "Nicolas", Age = 52, Male = true },
new Person { Name = "Susan", Age = 37, Male = false }
};
This way you don´t need to "synchronize" all three lists, you have only one list instead.
If you really must use the approach you described define a class holding three different lists:
class Persons
{
public List<string> Names { get; set; }
public List<int> Ages { get; set; }
public List<bool> Male { get; set; }
}
Now you can create your persons as follows:
var persons = new Persons {
Names = new List<string> { "James", "Nicolas", "Susan"},
Ages = new List<int> { 17, 53, 37 },
Male = new List<bool> { true, true, false }
}
However this is quite difficult as every time you delete a name for example you´d also have to delete the appropriate age- and male-element also. Something like this:
persons.Names.RemoveAt(1);
persons.Ages.RemoveAt(1);
persons.Male.RemoveAt(1);
As #HimBromBeere said you may create a Person class:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public bool Male { get; set; }
}
Now you need to define another class that would store a result
public class Result
{
public List<string> Names { get; } = new List<string>();
public List<int> Age { get; } = new List<int>();
public List<bool> Male { get; } = new List<bool>();
}
At this time you can convert list of persons to your expected output with Linq
var persons = new List<Person> {
new Person { Name = "James", Age = 18, Male = true },
new Person { Name = "Nicolas", Age = 52, Male = true },
new Person { Name = "Susan", Age = 37, Male = false }
};
var result = persons.Aggregate(new Result(), (c, n) =>
{
c.Names.Add(n.Name);
c.Age.Add(n.Age);
c.Male.Add(n.Male);
return c;
});
If you are using C# 3 or higher you can use anonymous objects:
var x = new
{
Names = new[] {"James", "Nicolas", "Susan"},
Age = new[] {18, 52, 37},
Male = new[] {true, true, false}
};
for your second example you may use this code:
var y = new
{
x = new
{
Names = new[] {"James", "Nicolas", "Susan"},
Age = new[] {18, 52, 37},
Male = new[] {true, true, false}
},
// z = new { /* same code as above */ }
};
How can you make a filter in LINQ, where a query groups only two subsequent rows, if their Level column is ascended by one.
This is what I have at this moment:
var alert = db.Logs.OrderBy(u => u.Time).GroupBy(r => r.EquipmentNr).Where(s => s.Count() > 1);
However it takes all the rows it does not find those, which are different in the Level column, and they must be different over time. There must be rows next to each other, ordered by time.
Or should I create a grouping class and manually, run through the query I already have and add whether I find them?
public class Log
{
public int Id { get; set; }
public DateTime Time { get; set; }
public Shift Shift { get; set; }
public int EquipmentNr { get; set; }
public int OrderNr { get; set; }
public bool SupervisorCalled { get; set; }
public string Issue { get; set; }
public string Repairs { get; set; }
public string Responsible { get; set; }
public Level Level { get; set; }
}
Most of the times, for sequential processing where elements in a list are somehow related, a classic foreach is much better than LINQ. Often it can't even be done by LINQ. However, in this case only successive elements are to be compared, so Aggregate can be used.
The grouping condition is: level(n) - level(n-1) = 1. So we should add identical grouping keys to each element in the list meeting this condition. Here's a way to do that (using Linqpad). Each element is stored in a Tuple where Item1 is the grouping key. Then the tuples are grouped by these keys:
void Main()
{
var list = new List<Equipment>
{
new Equipment{Level = 11},
new Equipment{Level = 1},
new Equipment{Level = 18},
new Equipment{Level = 0},
new Equipment{Level = 6},
new Equipment{Level = 4},
new Equipment{Level = 5},
new Equipment{Level = 20},
new Equipment{Level = 9},
new Equipment{Level = 14},
new Equipment{Level = 12},
new Equipment{Level = 17},
new Equipment{Level = 2},
new Equipment{Level = 13},
new Equipment{Level = 15},
};
list.OrderBy(eq => eq.Level)
.Aggregate
(
new List<Tuple<int,Equipment>>(), // Seed the list of tuples
(tuples,eq) =>
{
// The grouping condition: compare to the previous item
// (if present) and use its Level as grouping key if the
// condition is met.
var key = tuples.Any()
&& eq.Level - tuples.Last().Item2.Level == 1
? tuples.Last().Item1
: eq.Level;
tuples.Add(Tuple.Create(key, eq));
return tuples;
},
tuples => tuples.GroupBy (tuple => tuple.Item1)
// Done! Now just a Select for display
).Select
(tupleGroup =>
new {
tupleGroup.Key,
Numbers = string.Join(",", tupleGroup
.Select(e => e.Item2.Level))
}
). Dump();
}
// Define other methods and classes here
class Equipment
{
public int Level { get; set; }
}
The output:
Key Numbers
--- --------------
0 0,1,2
4 4,5,6
9 9
11 11,12,13,14,15
17 17,18
20 20