LINQ groupBy with multiple columns from string array [closed] - c#

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 8 years ago.
Improve this question
I have the following code:
string[] tokens; //columns which I want to groupBy (e.g., storeID, location)
Dictionary<string, List<DataRow>> dictShort = null; // what I want to return
switch (tokens.Length)
{
case 1:
dictShort = dsShort.Tables[0].AsEnumerable()
.GroupBy(x => x[block])
.Where(g => exp.GroupSizeOk((uint)g.Count()))
.OrderBy(g => g.Count())
.ToDictionary(g => g.Key.ToString(), g => g.ToList());
break;
case 2:
dictShort = (Dictionary<string, List<DataRow>>)
dsShort.Tables[0].AsEnumerable()
.GroupBy(x => x[tokens[0]], x => x[tokens[1]])
.Where(g => exp.GroupSizeOk((uint)g.Count()))
.OrderBy(g => g.Count())
.ToDictionary(g => g.Key.ToString(), g => g.ToList());
// NOT COMPILING> cannot convert Dictionary<string, List<objet>>
// to Dictionary<string, List<DataRow>>
break;
case 3:
dictShort = (Dictionary<string, List<DataRow>>)
dsShort.Tables[0].AsEnumerable()
.GroupBy(x => new { x[tokens[0]], x[tokens[1]], x[tokens[2]]})
.Where(g => exp.GroupSizeOk((uint)g.Count()))
.OrderBy(g => g.Count())
.ToDictionary(g => g.Key.ToString(), g => g.ToList());
// NOT COMPILING: invalid anonymous type member declarator
break;
}
My questions:
(1) Case 3 is not working, how can I correct it?
(2) Can I make this dynamic? (i.e., a foreach or something similar, so that it works with any number of colunms)
public bool GroupSizeOk(UInt32 size)
{
return (size >= _minGroupSize)
&& (_maxGroupSize > 0 ? size <= _maxGroupSize : true);
}

What you need is an IEqualityComparer for your array that will compare the arrays based on the items in them, rather than by the reference to the array itself. Such a comparer is simple enough to create:
public class SequenceComparer<T> : IEqualityComparer<IEnumerable<T>>
{
private IEqualityComparer<T> itemComparer;
public SequenceComparer(IEqualityComparer<T> itemComparer = null)
{
this.itemComparer = itemComparer ?? EqualityComparer<T>.Default;
}
public bool Equals(IEnumerable<T> x, IEnumerable<T> y)
{
return x.SequenceEqual(y);
}
public int GetHashCode(IEnumerable<T> obj)
{
unchecked
{
return obj.Aggregate(79,
(hash, next) => hash * 39 + next.GetHashCode());
}
}
}
Now your query will basically work as you intended it to work, with one other key change being transforming your array tokens into an array representing the values from that row that are in the given array:
var dictShort = dsShort.Tables[0].AsEnumerable()
.GroupBy(row => tokens.Select(token => row[token]).ToArray(),
new SequenceComparer<object>())
.Where(g => exp.GroupSizeOk((uint)g.Count()))
.OrderBy(g => g.Count())
.ToDictionary(g => string.Join(", ", g.Key), g => g.ToList());

You had:
.GroupBy(new x => { x[tokens[0]], x[tokens[1]], x[tokens[2]]})
But it should really be :
.GroupBy(x => string.Join(",",new [] { x[tokens[0]], x[tokens[1]], x[tokens[2]]}))
I had made the change in your question before I realized that this is part of your answer. The new goes inside of the lambda.
As for making the code dynamic based on columns, yes you can.
.GroupBy(x => string.Join(",",tokens.Where(w => w.Columns.Contains(w)).Select(s => x[s]).ToArray()))
That should give you a grouping of all the matching columns in the array.

I jsut found the awnswer:
dictShort = dsShort.Tables[0].AsEnumerable()
// This where selects elements if and only if all fields are not null
.Where(x => ListAnd(tokens.Select(t => x[t] != DBNull.Value && IsFilledIn(x[t].ToString())).ToArray()))
.GroupBy(x => String.Join("+", tokens.Select(t => x[t].ToString()).ToArray()))
//.GroupBy(x => x[block]) // TODO
.Where(g => exp.GroupSizeOk((uint)g.Count()))
.OrderBy(g => g.Count())
.ToDictionary(g => g.Key/*.ToString()*/, g => g.ToList());

Related

Additional square brackets in Linq result

In my DB, I have the following data:
track_id, point_id, latd, lond
A,1,12,100
A,2,13,101
B,1,10,90
B,2,13,90
I am trying to generate a IEnumerable<object[]> as follow:
[[12, 100],[13, 101]], [[10, 90],[13, 90]]
Which will be used in a multilinestring Geojson:
This is what I have tried:
var multiTrackList = _context.tracks
.GroupBy(g => g.track_id)
.Select(s => new object[] {
_context.tracks
.Where(w => w.track_id == s.Key)
.OrderBy(o => o.track_id).ThenBy(o => o.date).ThenBy(o => o.time)
.Select(e => new object[] { e.lond.Value, e.latd.Value }).ToList()
});
but it keeps returning:
[[[12, 100],[13, 101]]], [[[10, 90],[13, 90]]]
with extra unneeded square brackets. I don't see what I am doing wrong. Please help.
I'm not sure what you're trying to accomplish with all the grouping and ordering, but you're doing too many selects on your data source, generating [2][2][2] arrays. Maybe this will point you in the right direction:
context _context = new();
_context.tracks = new();
_context.tracks.Add(new track("A", 12, 100));
_context.tracks.Add(new track("A", 13, 101));
_context.tracks.Add(new track("B", 10, 90));
_context.tracks.Add(new track("B", 13, 90));
var multiTrackList = _context.tracks
.GroupBy(g => g.track_id) //grouping
.SelectMany(e => e.Where(x => x.track_id == e.Key)) //selection where track_id equals Keys on the grouped elements
.OrderBy(o => o.track_id) //ordering, I removed the date and time for simplicity
.Select(y => new object[2] { y.latd, y.lond }) //then you can generate your 2 positions array
.ToList(); //finally you can convert the IEnumerable into a List, or whatever you need
Console.Write(JsonConvert.SerializeObject(multiTrackList)); //"[[12,100],[13,101],[10,90],[13,90]]"
Thanks to #james-braz and after editing his answer, I was able to get the correct linq query:
var multiTrackList = _context.tracks
.GroupBy(g => g.track_id ) //grouping
.Select(e =>
e.Where(x => x.cruise_id == e.Key) //selection where track_id equals Keys on the grouped elements
.OrderBy(o => o.date).ThenBy(o => o.time) //ordering, I removed the date and time for simplicity
.Select(y => new object[2] { y.latd, y.lond })//then you can generate your 2 positions array
).ToList(); //finally you can convert the IEnumerable into a List, or whatever you need
I obtain the correct output ([[12,100],[13,101],[10,90],[13,90]]), I can use to update the coordinates property of my multilinestring GeoJSON

EF Core LINQ GROUPBY Then Select to get more than one properties of the entity

I have 2 tables Outlet and Order with below schemas:
Outlet Order
------ -------------------
Id Id
Name Name
OrderCompletedTime
NextOrderDueTime
OutletIds
Earlier when I wanted to get the NextOrderDueTime for each outlet using entity framework core, I did:
return _dbAccessor.RequestContext.MyDbContext.Order
.Where(i => i.OutletId == _dbAccessor.RequestContext.OutletId &&
!i.IsRemoved && i.NextOrderDueTime.HasValue)
.GroupBy(i => i.OutletId)
.Select(g => new { OutletId = g.Key, NextOrderDueTime = g.Min(x => x.NextOrderDueTime) })
.ToDictionary(i => i.OutletId, i => i.NextOrderDueTime);
Now on the UI we need to make this due time as link and wants user to get navigated to that Order details page based on order id
How can I change the above query to also return OrderId along with time?
My thoughts:
Change return type of method from Dictionary<int, DateTimeOffset?> to Dictionary<int, Tuple<int,DateTimeOffset?>>
I tried changing the Linq query to :
return _dbAccessor.RequestContext.MyDbContext.Order
.Where(i => i.OutletId == _dbAccessor.RequestContext.OutletId &&
!i.IsRemoved && i.NextOrderDueTime.HasValue)
.GroupBy(i => i.OutletId)
.Select(g =>
new
{
OutletId = g.Key,
NextOrderDueTime = g.FirstOrDefault(x => x.NextOrderDueTime == g.Min(y => y.NextOrderDueTime)).NextOrderDueTime,
NextOrderId = g.FirstOrDefault(x => x.NextOrderDueTime == g.Min(y => y.NextOrderDueTime)).OrderId
})
.ToDictionary(i => i.OutletId, i => new Tuple<int, DateTimeOffset?>(i.NextOrderId, i.NextOrderDueTime));
But this throws exception at runtime?
Please help to let me know what I am doing wrong here.
You could just take the entire order along with the outletid when you return:
.Select(g => new {
OutletId = g.Key,
NextOrder = g.OrderBy(x => x.NextOrderDueTime).FirstOrDefault()
})
You could select on this to take multiple properties from the order:
.Select(g => new {
OutletId = g.Key,
NextOrder = g.OrderBy(x => x.NextOrderDueTime).FirstOrDefault()
})
.Select(s => new {
s.OutletId,
NextOrderId = NextOrder.Id,
NextOrder.NextOrderDueTime,
NextOrderName = NextOrder.Name
})
etc..
The main thing to appreciate is that grouping gives you an object that has a key, but itself is a list of all things that have that key, so if you order the list by something like the DUeDat and take the first thing then you have an entire object with the lowest duedate from which you can take various things
The .GroupBy(...).Select(...).ToDictionary(...); cannot be converted to SQL since EF Core 3.0.
Due to the breaking change in EF Core 3.0. https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes , EF Core 3.0 will throw exception to make sure you know that all records in Order will be fetched from database before grouping and map to Dictionary.
I was able to get my query working as below:
return _dbAccessor.RequestContext.MyDbContext.Order
.Where(i => i.OutletId == _dbAccessor.RequestContext.OutletId &&
!i.IsRemoved && i.NextOrderDueTime.HasValue).AsEnumerable()
.GroupBy(i => i.OutletId)
.Select(g =>
new
{
OutletId = g.Key,
NextOrderDueTime = g.FirstOrDefault(x => x.NextOrderDueTime == g.Min(y => y.NextOrderDueTime)).NextOrderDueTime,
NextOrderId = g.FirstOrDefault(x => x.NextOrderDueTime == g.Min(y => y.NextOrderDueTime)).OrderId
})
.ToDictionary(i => i.OutletId, i => new Tuple<int, DateTimeOffset?>(i.NextOrderId, i.NextOrderDueTime));
The same can be done as shown in another answer by just adding AsEnumerable() before GroupBy:
_dbAccessor.RequestContext.MyDbContext.Order
.Where(i => i.OutletId == _dbAccessor.RequestContext.OutletId &&
!i.IsRemoved && i.NextOrderDueTime.HasValue).AsEnumerable()
.GroupBy(i => i.OutletId)
.Select(g => new {
OutletId = g.Key,
NextOrder = g.OrderBy(x => x.NextOrderDueTime).FirstOrDefault()
})
.Select(s => new {
s.OutletId,
NextOrderId = NextOrder.Id,
NextOrder.NextOrderDueTime,
NextOrderName = NextOrder.Name
})`enter code here`;

Using LINQ ANY and occurrence > 1

I have a list of objects containing a collection of child objects. I want to check if any of these objects children contain a specific value AND if they do then check if this value appears in any other objects.
I currently have this which needs to be extended for the second check:
public bool HasAnyTypesMoreThanOnce(IEnumerable<Parent> parents, IEnumerable<string> Types)
{
return parents
.SelectMany(p => p.Children)
.Any(c => Types.Contains(c.Type));
}
I was thinking of something like this:
var list = parents
.SelectMany(p => p.children)
.Where(c => Types.Contains(c.Type))
.Select(c => c.Type).ToList();
return list.Count != list.Distinct().Count();
Any suggestions?
Another approach would be to group by Type and return true if theres any Type repeating more than once otherwise false.
return parents.SelectMany(p => p.children)
.Where(c => Types.Contains(c.Type))
.GroupBy(x => x.Type)
.Any(g => g.Count() > 1);
Maybe something like this
parents
.GroupBy(c => c.children.type)
.Where(grp => grp.Count() > 1)
.Select(grp => grp.children.type);

C# LINQ get count to dictionary

I got a LINQ query with Entity Framework (EF) and getting a list of items. Now I want to create a dictionary with the incrementing index of the item and the item itself.
I have it like this:
result = context
.Items
.Where(b => !b.Deleted)
.OrderBy(b => b.Name)
.ToDictionary(COUNTHERE, b => b.Name)
So the dictionary have to look like this:
1: "item1"
2: "item2"
3: "item5"
4: "item10"
5: "item100"
I think what you need is to have the item name as the key instead the count as the key, because if there is two items that have the same count, it will throw exception that the key has been added.
Then you can Use GroupBy before ToDictionary so that you can count it.
result = context
.Items
.Where(b => !b.Deleted)
.GroupBy(x => x.Name)
.ToDictionary(g => g.Key, g => g.Count())
.OrderBy(g => g.Key);
based on your updated comment, then what you need is
result = context
.Items
.Where(b => !b.Deleted)
.OrderBy(b => b.Name)
.AsEnumerable()
.Select((v,i) => new { i, v.Name })
.ToDictionary(g => g.i + 1, g => g.Name);
Note that you need to add AsEnumerable so that the Select clause works as linq to object (Select that accept index is not supported in L2S).
Just use:
int count = 0;
var result = context
.Items
.Where(b => !b.Deleted)
.OrderBy(b => b.Name)
.ToDictionary(b => ++count, b => b.Name);
An alternative way of achieving this is:
var sortedNames = context
.Items
.Where(b => !b.Deleted)
.Select(b => b.Name)
.OrderBy(b => b.Name)
.ToArray();
result = Enumerable.Range(1, sortedNames.Length)
.ToDictionary(i => i, i => sortedNames[i - 1]);
To get zero-based numbering, use Enumerable.Range(0, sortedNames.Length) and sortedNames[i] instead.

Dictionaries: An item with the same key has already been added

In my MVC app I am using 2 dictionaries to populate SelectList for DropDownList. Those dictionaries will be supplied with dates as string and datetime values.
I have this chunk of code for the first dictionary that works just fine:
if (m_DictDateOrder.Count == 0)
{
m_DictDateOrder = new Dictionary<string, DateTime>();
m_DictDateOrder =
m_OrderManager.ListOrders()
.OrderBy(x => x.m_OrderDate)
.Distinct()
.ToDictionary(x => x.m_OrderDate.ToString(), x => x.m_OrderDate);
}
But when I get to the second dictionary:
if (m_DictDateShipped.Count == 0)
{
m_DictDateShipped = new Dictionary<string, DateTime>();
m_DictDateShipped =
m_OrderManager.ListOrders()
.OrderBy(x => x.m_ShippedDate)
.Distinct()
.ToDictionary(x => x.m_ShippedDate.ToString(), x => x.m_ShippedDate);
}
I get a runtime error on the LINQ request for the second dictionary:
An item with the same key has already been added.
I first though that I add to instantiate a new dictionary (that's the reason for the "new" presence), but nope. What did I do wrong?
Thanks a lot!
You are Distinct'ing the rows, not the dates.
Do this instead:
if (m_DictDateShipped.Count == 0)
{
m_DictDateShipped = m_OrderManager.ListOrders()
//make the subject of the query into the thing we want Distinct'd.
.Select(x => x.m_ShippedDate)
.Distinct()
.ToDictionary(d => d.ToString(), d => d);
}
Don't bother sorting. Dictionary is unordered.
My standard pattern for this (since I have disdain for Distinct) is:
dictionary = source
.GroupBy(row => row.KeyProperty)
.ToDictionary(g => g.Key, g => g.First()); //choose an element of the group as the value.
You applied the Distinct to the order, not to the date. Try
m_OrderManager.ListOrders()
.OrderBy(x => x.m_ShippedDate)
.Select(x =>x.m_ShippedDate)
.Distinct()
.ToDictionary(x => x.ToString(), x => x);

Categories

Resources