implement dense rank with linq - c#

Using the following linq code, how can I add dense_rank to my results? If that's too slow or complicated, how about just the rank window function?
var x = tableQueryable
.Where(where condition)
.GroupBy(cust=> new { fieldOne = cust.fieldOne ?? string.Empty, fieldTwo = cust.fieldTwo ?? string.Empty})
.Where(g=>g.Count()>1)
.ToList()
.SelectMany(g => g.Select(cust => new {
cust.fieldOne
, cust.fieldTwo
, cust.fieldThree
}));

This does a dense_rank(). Change the GroupBy and the Order according to your need :)
Basically, dense_rank is numbering the ordered groups of a query so:
var DenseRanked = data.Where(item => item.Field2 == 1)
//Grouping the data by the wanted key
.GroupBy(item => new { item.Field1, item.Field3, item.Field4 })
.Where(#group => #group.Any())
// Now that I have the groups I decide how to arrange the order of the groups
.OrderBy(#group => #group.Key.Field1 ?? string.Empty)
.ThenBy(#group => #group.Key.Field3 ?? string.Empty)
.ThenBy(#group => #group.Key.Field4 ?? string.Empty)
// Because linq to entities does not support the following select overloads I'll cast it to an IEnumerable - notice that any data that i don't want was already filtered out before
.AsEnumerable()
// Using this overload of the select I have an index input parameter. Because my scope of work is the groups then it is the ranking of the group. The index starts from 0 so I do the ++ first.
.Select((#group , i) => new
{
Items = #group,
Rank = ++i
})
// I'm seeking the individual items and not the groups so I use select many to retrieve them. This overload gives me both the item and the groups - so I can get the Rank field created above
.SelectMany(v => v.Items, (s, i) => new
{
Item = i,
DenseRank = s.Rank
}).ToList();
Another way is as specified by Manoj's answer in this question - But I prefer it less because of the selecting twice from the table.

So if I understand this correctly, the dense rank is the index of the group it would be when the groups are ordered.
var query = db.SomeTable
.GroupBy(x => new { x.Your, x.Key })
.OrderBy(g => g.Key.Your).ThenBy(g => g.Key.Key)
.AsEnumerable()
.Select((g, i) => new { g, i })
.SelectMany(x =>
x.g.Select(y => new
{
y.Your,
y.Columns,
y.And,
y.Key,
DenseRank = x.i,
}
);

var denseRanks = myDb.tblTestReaderCourseGrades
.GroupBy(x => new { x.Grade })
.OrderByDescending(g => g.Key.Grade)
.AsEnumerable()
.Select((g, i) => new { g, i })
.SelectMany(x =>
x.g.Select(y => new
{
y.Serial,
Rank = x.i + 1,
}
));

Related

Evenly distribute males and females in list with LINQ

I have Class1 like:
{
string Name,
string Sex
}
And I have a List<Class1> with 100 items where 50 are Males and 50 are Females, how do I get 10 groups of 5Males and 5Females each with LINQ?
I already manage to get the list grouped in 10 groups but not distributed evenly by sex.
var foo = My100List.Select((person, index) => new {person, index})
.GroupBy(x => x.index%10)
.Select(i => new Group
{
Name= "Group" + i.Key,
Persons= i.Select(y => y.person).ToList()
});
The code above don't distribute by sex.
Try this (untested):
int groupSize = 5;
var foo = My100List.GroupBy(x => x.Sex)
.SelectMany(g => g.Select((x, i) => new { Person = x, Group = i / groupSize}))
.GroupBy(x => x.Group)
.Select(g => new Group
{
Name = "Group" + g.Key,
Persons = g.Select(x => x.Person).ToList()
});
EDIT
Tested and confirmed. The above code works.
Add .OrderBy for sex before the .Select
Tested and working:
var foo = My100List.OrderBy(p => p.Sex).Select((person, index) => new {person, index})
.GroupBy(x => x.index%10)
.Select(i => new Group
{
Name= "Group" + i.Key,
Persons= i.Select(y => y.person).ToList()
});

Append index number to duplicated string value in a list - by using Lambda

I have a IList<string>() which holds some string values, and there could be duplicated items in the list. What I want is to append a index number to end of the string to eliminate the duplication.
For example, I have these values in my list: StringA, StringB, StringC, StringA, StringA, StringB. And I want the result looks like: StringA1, StringB1, StringC, StringA2, StringA3, StringB2. I need to retain the original order in list.
Is there a way I can just use one Lambda expression?
You are looking for something like this:
yourList.GroupBy(x => x)
.SelectMany(g => g.Select((x,idx) => g.Count() == 1 ? x : x + idx))
.ToList();
Edit: If the element order matters, here is another solution:
var counts = yourList.GroupBy(x => x).ToDictionary(x => x.Key, x => x.Count());
var values = counts.ToDictionary(x => x.Key, x => 0);
var list = yourList.Select(x => counts[x] > 1 ? x + ++values[x] : x).ToList();
You can do:
List<string> list = new List<string> { "StringA", "StringB", "StringC", "StringA", "StringA", "StringB" };
var newList =
list.Select((r, i) => new { Value = r, Index = i })
.GroupBy(r => r.Value)
.Select(grp => grp.Count() > 1 ?
grp.Select((subItem, i) => new
{
Value = subItem.Value + (i + 1),
OriginalIndex = subItem.Index
})
: grp.Select(subItem => new
{
Value = subItem.Value,
OriginalIndex = subItem.Index
}))
.SelectMany(r => r)
.OrderBy(r => r.OriginalIndex)
.Select(r => r.Value)
.ToList();
and you will get:
StringA1,StringB1,StringC,StringA2,StringA3,StringB2
If you don't want to preserve order then you can do:
var newList = list.GroupBy(r => r)
.Select(grp => grp.Count() > 1 ?
grp.Select((subItem, i) => subItem + (i + 1))
: grp.Select(subItem => subItem))
.SelectMany(r => r)
.ToList();
This uses some lambda expressions and linq to do it, maintaining the order but I'd suggested a function with a foreach loop and yield return would be better.
var result = list.Aggregate(
new List<KeyValuePair<string, int>>(),
(cache, s) =>
{
var last = cache.Reverse().FirstOrDefault(p => p.Key == s);
if (last == null)
{
cache.Add(new KeyValuePair<string, int>(s, 0));
}
else
{
if (last.Value = 0)
{
last.Value = 1;
}
cache.Add(new KeyValuePair<string, int>(s, last.Value + 1));
}
return cache;
},
cache => cache.Select(p => p.Value == 0 ?
p.Key :
p.Key + p.Value.ToString()));

Adding more parameters to a Linq query

I'm using this query to count number of orders by date. I'm trying to add one more parameter that counts total products for each order, however I can't get it to work atm.
This is the essential part of a method that is suposed to return a list of 3 parameters (Date, TotalOrders and TotalProducts). Im using a Linq query to get a list with total order for each date, im wondering how to add my third parameter to the list "TotalProducts" and if i can do by adding one more search parameter in the Query. The foreach part below do not work propertly, it will return a list of TotalProducts but CreationDate will be the same for ech item in the list. I also have a feeling putting a foreach inside a foreach dosn't seem optimal for this:
var orders = _orderService.SearchOrderStatistics(startDateValue, endDateValue, orderStatus,
paymentStatus, shippingStatus, model.CustomerEmail, model.OrderGuid);
var result = orders.Where(o => o.PaymentStatus == PaymentStatus.Paid)
.GroupBy(g => g.CreatedOnUtc.Date.ToString("yyyyMMdd"))
.Select(s => new { Date = s.Key, Count = s.Count() });
List<GCOrdersModel> TotalOrdersPaid = new List<GCOrdersModel>();
foreach (var g in result)
{
foreach (var opv in orders)
{
GCOrdersModel _Om = new GCOrdersModel(g.Date, g.Count.ToString(), opv.OrderProductVariants.Count.ToString());
TotalOrdersPaid.Add(_Om);
}
}
return TotalOrdersPaid;
To access total products for every orders I must use OrderProductVariants.Count.ToString()
Can I add this parameter to the query?
Thx
You could try this:
return orders.Where(o => o.PaymentStatus == PaymentStatus.Paid)
.GroupBy(g => g.CreatedOnUtc.Date.ToString("yyyyMMdd"))
.Select(s => new GCOrdersModel()
{
Date = s.Key,
Count = s.Count(),
OpvCount = opv.OrderProductVariants.Count.ToString()
})
.ToList();
or
return orders.Where(o => o.PaymentStatus == PaymentStatus.Paid)
.GroupBy(g => g.CreatedOnUtc.Date.ToString("yyyyMMdd"))
.Select(s => new GCOrdersModel(s.Key, s.Count, opv.OrderProductVariants.Count.ToString()))
.ToList();
That way, you don't have to iterate over your result again. And it automatically creates your list of GCOrdersModel.
Edit
Does this work?
return orders.Where(o => o.PaymentStatus == PaymentStatus.Paid)
.GroupBy(g => g.CreatedOnUtc.Date.ToString("yyyyMMdd"))
.Select(s => new GCOrdersModel()
{
Date = s.Key,
Count = s.Count(),
OpvCount = s.OrderProductVariants.Count.ToString()
})
.ToList();
or
return orders.Where(o => o.PaymentStatus == PaymentStatus.Paid)
.GroupBy(g => g.CreatedOnUtc.Date.ToString("yyyyMMdd"))
.Select(s => new GCOrdersModel(s.Key, s.Count(), s.OrderProductVariants.Count.ToString()))
.ToList();
How about:
var opvCount =
opv
.OrderProductVariants
.Count
.ToString();
return
orders
.Where(o => o.PaymentStatus == PaymentStatus.Paid)
.GroupBy(g => g.CreatedOnUtc.Date.ToString("yyyyMMdd"))
.Select(s => new
{
Date = s.Key,
Count = s.Count()
})
.Select(x =>
new GCOrdersModelg(x.Date, g.Count.ToString(), opvCount));

Cannot Group By on multiple columns and Count

I want to write this simple query with Linq:
select issuercode,securitycode,dataprocessingflag,COUNT(issuercode) as cnt
from cmr_invhdr
where ProcessedLike <> 'STMNT ONLY'
group by issuercode,securitycode,dataprocessingflag
order by Issuercode
I've tried the following code but I get this error( DbExpressionBinding requires an input expression with a collection ResultType.
Parameter name: input) :
var lstCMRInvHdrNips = (from r in e.CMR_INVHDR
where r.ProcessedLike != "STMNT ONLY"
select new {
r.IssuerCode,
r.SecurityCode,
CountofIssuerCode = r.IssuerCode.Count(),
r.DataProcessingFlag
}
).GroupBy(x =>
new {
x.IssuerCode,
x.SecurityCode,
x.DataProcessingFlag,
x.CountofIssuerCode
}
).OrderBy(x => x.Key.IssuerCode).ToList();
Is there any sense to count issuercode while grouping by this field at once? As when groupped by a field, it's COUNT will always be 1.
Probably you should not group by issuercode and count it after the GroupBy in a separate Select statement:
var result = e.CMR_INVHDR
.Where(r => r.ProcessedLike != "STMNT ONLY")
.GroupBy(r => new { r.SecurityCode, r.DataProcessingFlag })
.Select(r => new
{
Value = r.Key,
IssuerCodesCount = r.GroupBy(g => g.IssuerCode).Count()
})
.ToList();

How to compare 2 list by characters content and its correspondents double values?

I have 2 lists: a string list and a double list with same length and with same index of correspondence. I need to compare all the strings, find the indexes of the list that has the same characters, independent of its order, and delete the highest double value that corresponds to both,
Example:
List<string> str= new List<string>();
str.add("efc");
str.add("abc");
str.add("cde");
str.add("cab");
str.add("fbc");
List<double> vlr= new List<double>();
vlr.add(0.1);
vlr.add(0.5);
vlr.add(0.4);
vlr.add(0.2);
vlr.add(0.3);
and this case, "abc" => (0.5) must be deleted because "cab" has the same characters AND lower correspondent value =>(0.2).
There is a lambda expression for this 2 arrays??
What I've tried:
var distinct = list .Select((str, idx) => new { Str = str, Idx = idx })
.GroupBy(pair => new HashSet<char>(pair.Str), HashSet<char>.CreateSetComparer())
.Select(grp => grp.OrderBy(p => p.Idx).First())
.ToList();
Here's one way to solve it:
// Pair the strings with their correspondence values
var pairs = str.Zip(vlr, (s, d) => new {s, d});
// Group using a sorted string, eliminating differences due to character order
var groups = pairs.GroupBy(x => new string(x.s.ToCharArray().OrderBy(c => c).ToArray()));
// For each group, retain the item with the lowest correspondence value
var filtered = groups.Select(x => x.OrderBy(y => y.d).First().s);
var newDict = str.Zip(vlr, (s, d) => new { s, d })
.GroupBy(x => String.Join("", x.s.OrderBy(y => y)))
.Select(g => g.OrderBy(x => x.d).First())
.ToDictionary(x => x.s, x => x.d);
here is the code:
var group = str.GroupBy(s => string.Join("", s.ToCharArray().OrderBy(c => c)));
var _vlr = group.Select(g => g.Min(s => vlr[str.IndexOf(s)]));
var _str = group.Select(g => g.OrderBy(s => vlr[str.IndexOf(s)]).First());
and the result:

Categories