Converting SQL Rank() to LINQ, or alternative - c#

I have the below SQL statement that works as desired/expected. However I would like to translate it into a LINQ statement(Lambda??) so that it will fit with the rest of my DAL. However I cannot see to figure out how to simulate Rank() in LINQ.
The reason I posted it here, which is maybe in error, is to see if anyone has an alternative to the Rank() statement so that I can get this switched over. Alternatively, if there is a way to represent Rank() in LINQ that would be appreciated also.
USE CMO
SELECT vp.[PersonID] AS [PersonId]
,ce.[EnrollmentID]
,vp.[FirstName]
,vp.[LastName]
,ce.[EnrollmentDate]
,ce.[DisenrollmentDate]
,wh.WorkerCategory
FROM [dbo].[vwPersonInfo] AS vp
INNER JOIN
(
[dbo].[tblCMOEnrollment] AS ce
LEFT OUTER JOIN
(
SELECT *
,RANK()OVER(PARTITION BY EnrollmentID ORDER BY CASE WHEN EndDate IS NULL THEN 1 ELSE 2 END, EndDate DESC, StartDate DESC) AS whrank
FROM [dbo].[tblWorkerHistory]
WHERE WorkerCategory = 2
) AS wh
ON ce.[EnrollmentID] = wh.[EnrollmentID] AND wh.whrank = 1
)
ON vp.[PersonID] = ce.[ClientID]
WHERE (vp.LastName NOT IN ('Client','Orientation','Real','Training','Matrix','Second','Not'))
AND (
(wh.[EndDate] <= GETDATE())
OR wh.WorkerCategory IS NULL
)
AND (
(ce.[DisenrollmentDate] IS NULL)
OR (ce.[DisenrollmentDate] >= GetDate())
)

Here's a sample that shows how I would simulate Rank() in Linq:
var items = new[]
{
new { Name = "1", Value = 2 },
new { Name = "2", Value = 2 },
new { Name = "3", Value = 1 },
new { Name = "4", Value = 1 },
new { Name = "5", Value = 3 },
new { Name = "6", Value = 3 },
new { Name = "7", Value = 4 },
};
var q = from s in items
orderby s.Value descending
select new
{
Name = s.Name,
Value = s.Value,
Rank = (from o in items
where o.Value > s.Value
select o).Count() + 1
};
foreach(var item in q)
{
Console.WriteLine($"Name: {item.Name} Value: {item.Value} Rank: {item.Rank}");
}
OUTPUT
Name: 7 Value: 4 Rank: 1
Name: 5 Value: 3 Rank: 2
Name: 6 Value: 3 Rank: 2
Name: 1 Value: 2 Rank: 4
Name: 2 Value: 2 Rank: 4
Name: 3 Value: 1 Rank: 6
Name: 4 Value: 1 Rank: 6

LINQ has rank funcionality built in, but not in the query syntax. When using the method syntax most linq functions come in two versions - the normal one and one with a rank supplied.
A simple example selecting only every other student and then adding the index in the resulting sequence to the result:
var q = class.student.OrderBy(s => s.studentId).Where((s, i) => i % 2 == 0)
.Select((s,i) => new
{
Name = s.Name,
Rank = i
}

If you want to simulate rank then you can use following linq query.
var q = (from s in class.student
select new
{
Name = s.Name,
Rank = (from o in class.student
where o.Mark > s.Mark && o.studentId == s.studentId
select o.Mark).Distinct().Count() + 1
}).ToList();
you can use order by like:
var q = (from s in class.student
orderby s.studentId
select new
{
Name = s.Name,
Rank = (from o in class.student
where o.Mark > s.Mark && o.studentId == s.studentId
select o.Mark).Distinct().Count() + 1
}).ToList();
but order by does not matter in this query.

Based on answer from #Totero but with a lamda implementation.
Higher score = higher rank.
var rankedData = data.Select(s => new{
Ranking = data.Count(x => x.Value > s.Value)+1,
Name = s.Key,
Score = s.Value});
For this input:
{ 100, 100, 98, 97, 97, 97, 91, 50 }
You will get this output:
Score : Rank
100 : 1
100 : 1
98 : 3
97 : 4
97 : 4
97 : 4
91 : 6
50 : 7

Related

Perform a lookup between C# Lists and return matched where value is minimum

I have two lists:
var qtys = new List<InventoryQuantity>()
{
new InventoryQuantity() { WarehouseId = 1, QuantityInWarehouse = 0 },
new InventoryQuantity() { WarehouseId = 2, QuantityInWarehouse = 452 },
new InventoryQuantity() { WarehouseId = 3, QuantityInWarehouse = 184 },
new InventoryQuantity() { WarehouseId = 4, QuantityInWarehouse = 328 },
new InventoryQuantity() { WarehouseId = 5, QuantityInWarehouse = 0 },
};
var times = new List<WarehouseTransitTime>()
{
new WarehouseTransitTime() { WarehouseId = 1, TransitDays = 1 },
new WarehouseTransitTime() { WarehouseId = 2, TransitDays = 4 },
new WarehouseTransitTime() { WarehouseId = 3, TransitDays = 2 },
new WarehouseTransitTime() { WarehouseId = 4, TransitDays = 3 },
new WarehouseTransitTime() { WarehouseId = 5, TransitDays = 5 },
};
class InventoryQuantity
{
public int WarehouseId { get; set; }
public int QuantityInWarehouse { get; set; }
}
class WarehouseTransitTime
{
public int WarehouseId { get; set; }
public int TransitDays { get; set; }
}
I need to return the WarehouseId from qtys where the Quantity > 0 and the WarehouseId equals the minimum transit days WarehouseId in times.
I know I can do something like below but seems clunky and there must be an elegant solution.
public int NearestWarehouse()
{
var withQty = qtys.Where(i => i.QuantityInWarehouse > 0);
var orderedTransit = times.OrderBy(tt => tt.TransitDays).ToList();
//loop and compare
}
Example data:
qtys
WarehouseId | Quantity
1 | 0
2 | 452
3 | 184
4 | 328
5 | 0
times
WarehouseId | TransitTime
1 | 1
2 | 4
3 | 2
4 | 3
5 | 5
Expected output would be 3, because warehouse 3 has inventory and the shortest transit time (2)
It seems to me that the cleanest and simplest query is this:
var query =
from q in qtys
where q.QuantityInWarehouse > 0
join t in times on q.WarehouseId equals t.WarehouseId
orderby t.TransitDays
select q.WarehouseId;
var warehouseId = query.FirstOrDefault();
This gives me 3.
What you want is a group join:
Functional Syntax
var query1 = qtys.Where(q => q.QuantityInWarehouse > 0)
.GroupJoin(times, q => q.WarehouseId, t => t.WarehouseId, (q, t) => new { q.WarehouseId, TransitDays = t.DefaultIfEmpty().Min(grp => grp?.TransitDays) })
.OrderBy(g => g.TransitDays)
.FirstOrDefault();
Query Syntax
var query2 = from q in qtys
join t in times on q.WarehouseId equals t.WarehouseId into grp
where q.QuantityInWarehouse > 0
select new
{
q.WarehouseId,
TransitDays = grp.DefaultIfEmpty().Min(g => g?.TransitDays)
};
var result = query2.OrderBy(g => g.TransitDays)
.FirstOrDefault();
A group join will join two lists together on their corresponding keys--similar to a database join--and the associated values to those keys will be grouped into an enumerable. From that enumerable, you can derive the minimum value that you care about, TransitDays in this case.
There is no equivalent to "first or default" in query syntax. The easiest approach is just to apply the same OrderBy and FirstOrDefault against the query variable, as demonstrated above.
Well you mention an AND relation between the two, right?
I was thinking of databases with a forignkey... but Linq prety much does it if your lists aren't to big:
keys = qtys.Where(i => i.QuantityInWarehouse > 0).Select(i => i.WarehouseId).ToList();
// get the smallest not a list
var result = times.Where(tt => keys.Contains(tt.wharehouseId)).orderby(tt => tt.Transitdays).FirstOrDefault();
Otherwise you could have Dictionary with ID as key...
You can do it like this..
var withQty = (from q in qtys
join t in times on q.WarehouseId equals t.WarehouseId
where q.QuantityInWarehouse > 0
select new { q.WarehouseId, t.TransitDays })
.OrderBy(item => item.TransitDays).FirstOrDefault();
return withQty?.WarehouseId ?? 0;

Select TOP 1 for each FK in list using Entity Framework

I have a large table where I'm trying to select the top 1 row for each FK in a list.
My table is laid out as:
ChangeId | AssetId | Timestamp
1 1 123
2 2 999
3 1 3478
4 3 344
5 2 1092
Where ChangeId is my PK, AssetId is my FK and Timestamp is the value I'm trying to select.
If I try the following:
var results =
from Asset in _context.Asset
join change in _context.Change on Asset.AssetId equals change.AssetId into potentialChange
from actualChange in potentialChange.OrderByDescending(y => y.ChangeId).Take(1)
select
{
AssetId,
Timestamp
}
Where my expected result would be:
[
{
AssetId: 1,
Timestamp: 3478
},
{
AssetId: 2,
Timestamp: 1092
},
{
AssetId: 3,
Timestamp: 344
}
]
This query flags up the The LINQ expression could not be translated and will be evaluated locally. which is not suitable for a production rollout.
Running a foreach loop and selecting each item out 1 by 1 works, not it's not a performant solution.
Is there a suitable way to achieve the above?
Try to group it by AssetId and take max from each group
var results =
from Asset in _context.Asset
join change in _context.Change on Asset.AssetId equals change.AssetId into potentialChange
group potentialChange by potentialCharge.AssetId into g
select
{
g.Key,
g.Max().Timestamp
}
Use Group By as follows:
List<MyTable> data = new List<MyTable>()
{
new MyTable(){ChangeId = 1, AssetId = 1, Timestamp = 123},
new MyTable(){ChangeId = 2, AssetId = 2, Timestamp = 999},
new MyTable(){ChangeId = 3, AssetId = 1, Timestamp = 123},
new MyTable(){ChangeId = 5, AssetId = 3, Timestamp = 123},
new MyTable(){ChangeId = 5, AssetId = 2, Timestamp = 123},
};
var expectedData = data.OrderByDescending(d => d.Timestamp).GroupBy(d => d.AssetId).Select(g => new
{
AssetId = g.Key,
TimeStamp = g.First().Timestamp
}).ToList();
This will give your expected result.
Try using .First() instead of .Take(1)
LINQ How to take one record and skip rest c#

How To Find Highest Value In A Row And Return Column Header and it's value

Imagine a row of 5 numeric values in an Entity Framework database, how would I retrieve the top 2 columns of that row including the name of the column and their values? Preferably using LINQ.
For example:
a b c d e
0 4 5 9 2
The top 2 values are 9 and 5. I would like to retrieve the values and the column names, c and d.
A more practical example:
var row = table.Where(model => model.Title.Contains(a.Title));
This line will give me a single row with many numeric values.
I would like something as follows,
row.list().OrderByDescendingOrder().top(2);
I don't know how you do this in linq, but here is a SQL Server query:
select t.*, v2.*
from t cross apply
(values ('a', a), ('b', b), ('c', c), ('d', d), ('e', e)
) v(col, val) cross apply
(select max(case when seqnum = 1 then val end) as val1,
max(case when seqnum = 1 then col end) as col1,
max(case when seqnum = 2 then val end) as val2,
max(case when seqnum = 3 then col end) as col2
from (select v.*, row_number() over (order by val desc) as seqnum
from v
) v
) v2;
EDIT:
Of course, you can do this with massive case expressions to get the maximum value:
select t.*,
(case when a >= b and a >= c and a >= d and a >= e then a
when b >= c and b >= d and b >= e then b
when c >= d and c >= e then c
when d >= e then d
else e
end) as max_value,
(case when a >= b and a >= c and a >= d and a >= e then 'a'
when b >= c and b >= d and b >= e then 'b'
when c >= d and c >= e then 'c'
when d >= e then 'd'
else 'e'
end) as max_value_col
from t;
The problem is extending this to the second value, particularly if there are duplicate values.
It looks like you want to pivot this data, but using Linq instead of T-SQL. (or some other SQL dialect)
The basic pattern to do this is to use a SelectMany to transform each row to
an array of key/value pairs that you can do an OrderByDescending on.
A somewhat generic pattern for this is below:
// The values that we want to query.
// In your case, it's essentially the table that you're querying.
// I used an anonymous class for brevity.
var values = new[] {
new { key = 99, a = 0, b = 4, c = 5, d = 9, e = 2 },
new { key = 100, a = 0, b = 5, c = 3, d = 2, e = 10 }
};
// The query. I prefer to use the linq query syntax
// for actual SQL queries, but you should be able to translate
// this to the lambda format fairly easily.
var query = (from v in values
// Transform each value in the object/row
// to a name/value pair We include the key so that we
// can distinguish different rows.
// Because we need this query to be translated to SQL,
// we have to use an anonymous class.
from column in new[] {
new { key = v.key, name = "a", value= v.a },
new { key = v.key, name = "b", value= v.b },
new { key = v.key, name = "c", value= v.c },
new { key = v.key, name = "d", value= v.d },
new { key = v.key, name = "e", value= v.e }
}
// Group the same row values together
group column by column.key into g
// Inner select to grab the top two values from
// each row
let top2 = (
from value in g
orderby value.value descending
select value
).Take(2)
// Grab the results from the inner select
// as a single-dimensional array
from topValue in top2
select topValue);
// Collapse the query to actual values.
var results = query.ToArray();
foreach(var value in results) {
Console.WriteLine("Key: {0}, Name: {1}, Value: {2}",
value.key,
value.name,
value.value);
}
However, since you have a single row, the logic becomes much more simple:
// The value that was queried
var value = new { key = 99, a = 0, b = 4, c = 5, d = 9, e = 2 };
// Build a list of columns and their corresponding values.
// You could even use reflection to build this list.
// Additionally, you could use C# 7 tuples if you prefer.
var columns = new[] {
new { name = "a", value = value.a },
new { name = "b", value = value.b },
new { name = "c", value = value.c },
new { name = "d", value = value.d },
new { name = "e", value = value.e }
};
// Order the list by value descending, and take the first 2.
var top2 = columns.OrderByDescending(v => v.value).Take(2).ToArray();
foreach(var result in top2) {
Console.WriteLine("Column: {0}, Value: {1}", result.name, result.value);
}
So you have a collection of items, which can be sorted on one of its properties and you want the two items in the collection with the largest value for this sort property?
var result = myItems
.OrderByDescending(myItem => myItem.MyProperty)
.Take(2);
In words: Order the complete collection in a descending order of MyProperty and take the first two items from the result, which will be the two with the largest value for MyProperty.
This will return two complete myItem objects. Usually it is not a good idea to transfer more properties from objects to local memory than you'll actually plan to use. Use a Select to make sure that only the values you plan to use are transferred:
var bestSellingProducts = products
.OrderByDescending(product=> product.Orders.Count())
.Select(product => new
{ // select only the properties you plan to use, for instance
Id = product.Id,
Name = product.Name,
Stock = product.Stock
Price = product.Price,
});
.Take(2);

merging 2 lists into 1 with logic

I have 2 lists of the same type.
List 1:
ID
Name
Value
1,"Prod1", 0
2,"Prod2", 50
3,"Prod3", 0
List 2:
ID
Name
Value
1,"Prod1", 25
2,"Prod2", 100
3,"Prod3", 75
I need to combine these 2 lists into 1, but I only want the values from list2 if the corresponding value from list1 == 0
So my new list should look like this:
1,"Prod1", 25
2,"Prod2", 50
3,"Prod3", 75
I've tried many variations of something like this:
var joined = from l1 in List1.Where(x=>x.Value == "0")
join l2 in List2 on l1.ID equals l2.ID into gj
select new { gj };
I've also tried a variation of the concat
What is the best way of doing this?
You just need to select the individual properties and conditionally select either the Value from the first or second list item.
var List1 = new[]
{
new { Name = "Prod1", Id = 1, Value = 0 },
new { Name = "Prod2", Id = 2, Value = 50 },
new { Name = "Prod3", Id = 3, Value = 0 },
new { Name = "NotInList2", Id = 4, Value = 0}
};
var List2 = new[]
{
new { Name = "Prod1", Id = 1, Value = 25 },
new { Name = "Prod2", Id = 2, Value = 100 },
new { Name = "Prod3", Id = 3, Value = 75 }
};
var results = from l1 in List1
join l2temp in List2 on l1.Id equals l2temp.Id into grpj
from l2 in grpj.DefaultIfEmpty()
select new
{
l1.Id,
l1.Name,
Value = l1.Value == 0 && l2 != null ? l2.Value : l1.Value
};
foreach(var item in results)
Console.WriteLine(item);
Will output
{ Id = 1, Name = Prod1, Value = 25 }
{ Id = 2, Name = Prod2, Value = 50 }
{ Id = 3, Name = Prod3, Value = 75 }
{ Id = 4, Name = NotInList2, Value = 0 }
NOTE: This assumes that you only want all the ids that are in List1 (not any that are only in List2) and that the ids are unique and that the Name from List1 is what you want even if it is different in List2.
clone l1 and
foreach (var item in l1Clone)
if (item.value == 0)
item.value == l2.FirstOrDefault(l2item => l2item.ID == item.ID)
Refer to the code below:
IEnumerable<item> join_lists(IEnumerable<item> list1, IEnumerable<item> list2)
{
var map = list2.ToDictionary(i => i.id);
return list1.Select(i => new item()
{
id = i.id,
name = i.name,
value = i.value == 0 ? map[i.id].value : i.value
});
}
You could use Zip:
var combined = list1
.Zip(list2, (product1, product2) => product1.Value == 0 ? product2 : product1);

How to get sum of data in a list using multiple column values

I have a list using this Linq query
filterEntities = (from list in filterEntities where list.Id== 0 && list.Id== 1 && list.Id == 3 && list.Id== 6 select list).OrderBy(r => r.Id).ToList();
Now this linq returns a list like
ID Age
0 18
0 19
1 21
3 24
6 32
6 08
I want to generate a list using sum of same Id's which returns like
ID Age
0 37
1 21
3 24
6 40
Please suggest me possible query
I think you are looking to use a group by like this
List<int> ids = new List<int>() { 0, 1, 3, 6 };
filterEntities = (from list in filterEntities
where ids.Contains(list.Id)
group list by list.id into g
orderby g.Key
select new
{
ID = g.Key,
Age = g.Sum(x => x.Age),
}).ToList();
I would clean up the query like this, because the long expression looks a bit confusing:
var idList = new List<int> { 0, 1, 3, 6};
filterEntities = from e in filterEntities
where idList.Contains(e.Id)
group e by e.Id into g
select new { Id = g.Key, Sum = g.Sum(e =>e.Age) };
filterEntities = filterEntities.Where(l=>new[] { 0, 1, 3, 6 }.Contains(l.Id))
.Sum(c=>c.Age)
.GroupBy(r=>r.Id)
.ToList();

Categories