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
Related
I have 2 lists: list and listLookup
How do I update all the ValueToGet in list with closes KeyLookup in listLookup?
https://dotnetfiddle.net/QHd0Rr
using System.Collections.Generic;
public class Program
{
public static void Main()
{
//Arrange
//Keys needed to get ValueToGet property
var key1 = new WidgetA{Id = 1, Key = 52};
var key2 = new WidgetA{Id = 2, Key = 102};
var key3 = new WidgetA{Id = 3, Key = 152};
List<WidgetA> list = new List<WidgetA>();
list.Add(key1);
list.Add(key2);
list.Add(key3);
//Lookups
var keyLookup1 = new WidgetB()
{Id = 1, KeyLookup = 50, ValueLookup = "Fifty"};
var keyLookup2 = new WidgetB()
{Id = 2, KeyLookup = 100, ValueLookup = "One hundred"};
var keyLookup3 = new WidgetB()
{Id = 3, KeyLookup = 150, ValueLookup = "One hundred and fifty"};
List<WidgetB> listLookup = new List<WidgetB>();
listLookup.Add(keyLookup1);
listLookup.Add(keyLookup2);
listLookup.Add(keyLookup3);
//Act
/* Update all rows for ValueToGet property in list, using the closes KeyLookup in listLookup
Expected result:
key1: Key = 52, ValueToGet = "Fifty"
key2: Key = 102, ValueToGet = “One hundred”
key3: Key = 152, ValueToGet = “One hundred and fifty”
*/
}
}
public class WidgetA
{
public int Id { get; set; }
public int Key { get; set; }
public string ValueToGet { get; set; }
}
public class WidgetB
{
public int Id { get; set; }
public int KeyLookup { get; set; }
public string ValueLookup { get; set; }
}
In SQL it would be kind of like this but finding the closest key somehow:
update list
set ValueToGet = ValueLookup
from list l1
join listLookup l2
on l1.key = l2.keyLookup
If we define closest as:
Take the KeyLookup that is lower than the current Key
If there is no KeyLookup lower than the current key, take the closest higher one
Than this would solve your problem
private static void AddValueLookup(IEnumerable<WidgetA> list, IEnumerable<WidgetB> listLookup)
{
// Ensure the collection is ordered
using var bENumerator = listLookup.OrderBy(b => b.KeyLookup).GetEnumerator();
// Get the first two values.
// If the collection does not have at least one value, return.
if(!bENumerator.MoveNext())
return;
var currentKey = bENumerator.Current;
bENumerator.MoveNext();
var nextKey = bENumerator.Current;
// Ensure the collection is ordered.
// If the list contains a key that is smaller, than the smallest one in the lookup list,
// it will get the next higher ValueLookup
foreach (var a in list.OrderBy(a => a.Key))
{
while (a.Key > nextKey.KeyLookup)
{
currentKey = nextKey;
if (bENumerator.MoveNext())
nextKey = bENumerator.Current;
else
break;
}
a.ValueLookup = currentKey.ValueLookup;
}
}
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.
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,
});
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 */ }
};
I have a large List<MyClass> of objects, around 600000.
MyClass has like 10 properties, let's say property1, property2, etc.. until property10.
Out of that list, I want to get a List of List<MyClass> with objects having the same value for some of the properties.
That means for example, objects where property2, property4, property8 and property10 are the same.
What is the best way to do that? Currently I do a loop over my List<MyClass>, and within that loop I get all similar objects via List<MyClass>.FindAll(), dummy code:
forach(var item in myClassList)
{
if(!found.Contains(item))
{
var similarObjects = myClassList.FindAll(x => x.property2 == item.property2 && x.property4 == item.property4 && x.property8 == item.property8 && x.property10 == item.property10);
//adding the objects to the "already found" list
foreach(var foundItem in similarOjbects)
{
found.Add(foundItem);
}
if(similarObjects.Count > 1)
{
similarObjectsList.Add(similarObjects);
}
}
}
But it takes ages, the List.FindAll() method is very slow.
Is there a more efficient algorithm to do that?
You can use group by to solve this quite efficiently:
var grouped =
from item in myClassList
group item
by new {item.Property2, item.Property4, item.Property8, item.Property10};
That will give you a sequence of groups where each group contains all the objects that have the same values for the specified properties.
As an example, to iterate over every item in each group of the resulting sequence of groups you could do something like this:
foreach (var group in grouped)
{
foreach (var item in group)
{
// Do something with item
}
}
Note that this assumes that the type of each property implements IEquatable<T> and GetHashCode().
Here's a compilable example:
using System;
using System.Collections.Generic;
using System.Linq;
namespace Demo
{
class Data
{
public string Name { get; set; }
public int Property1 { get; set; }
public int Property2 { get; set; }
public int Property3 { get; set; }
public int Property4 { get; set; }
public int Property5 { get; set; }
public int Property6 { get; set; }
public int Property7 { get; set; }
public int Property8 { get; set; }
public int Property9 { get; set; }
public int Property10 { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Data> myClassList = new List<Data>
{
new Data {Name = "1A", Property2 = 1, Property4 = 1, Property8 = 1, Property10 = 1},
new Data {Name = "1B", Property2 = 1, Property4 = 1, Property8 = 1, Property10 = 1},
new Data {Name = "1C", Property2 = 1, Property4 = 1, Property8 = 1, Property10 = 1},
new Data {Name = "2A", Property2 = 2, Property4 = 2, Property8 = 2, Property10 = 2},
new Data {Name = "2B", Property2 = 2, Property4 = 2, Property8 = 2, Property10 = 2},
new Data {Name = "2C", Property2 = 2, Property4 = 2, Property8 = 2, Property10 = 2},
new Data {Name = "3A", Property2 = 3, Property4 = 3, Property8 = 3, Property10 = 3},
new Data {Name = "3B", Property2 = 3, Property4 = 3, Property8 = 3, Property10 = 3},
new Data {Name = "3C", Property2 = 3, Property4 = 3, Property8 = 3, Property10 = 3},
};
var grouped =
from item in myClassList
group item
by new {item.Property2, item.Property4, item.Property8, item.Property10};
foreach (var group in grouped)
{
Console.WriteLine(string.Join(", ", group.Select(item => item.Name)));
}
}
}
}
The example above outputs:
1A, 1B, 1C
2A, 2B, 2C
3A, 3B, 3C
Possible optimisation using PLINQ
As mentioned by #BertPersyn below, you could perhaps speed this up using PLINQ.
To do that, simply use the following to generate grouped (note the addition of .AsParallel()):
var grouped =
from item in myClassList.AsParallel()
group item
by new {item.Property2, item.Property4, item.Property8, item.Property10};
To determine if this actually speeds things up, it is imperative that you perform some timings.
First add a method, GetUniqueKey, that returns a unique key (hash) in your class.
Then, use grouping to find items with similar key:
List<List<Item>> = items
.GroupBy(item => item.GetUniqueKey())
.Select(g => g.ToList())
.ToList();
The GetUniqueKey method should be implemented and optimized based on required properties type. For example, if Property1 and Property2 are integers, you may use the following method:
public string GetUniqueKey()
{
return Prop1.ToString() + "-" + Prop2.ToString();
}
OR (more optimized)
public object GetUniqueKey()
{
return new { P1 = Prop1, P2 = Prop2 };
}
The GetUniqueKey example method itself may not be optimized, you may find another optimized implementation.
Full Example:
class Item
{
public int Prop1 {get; set;}
public int Prop2 {get; set;}
public string GetUniqueKey()
{
return Prop1.ToString() + "-" + Prop2.ToString();
}
}
public void DoWork()
{
Random rnd = new Random();
List<Item> items = new List<Item>();
for(int i = 0; i < 600000; i++)
{
items.Add(new Item { Prop1 = rnd.Next(1, 10) });
}
for(int i = 0; i < 600000; i++)
{
items[i].Prop2 = rnd.Next(1, 13);
}
List<List<Item>> = items
.GroupBy(item => item.GetUniqueKey())
.Select(g => g.ToList())
.ToList();
}