How can I achieve SQL CASE statement from LINQ - c#

I am new to LINQ and I would like to know if I can achieve the below SQL query from LINQ?
I am using Entity Framework Core.
SELECT 0 [All], [Range] =
CASE
WHEN Value BETWEEN 0 AND 25 THEN 'Low'
WHEN Value BETWEEN 25 AND 75 THEN 'Medium'
WHEN Value BETWEEN 75 AND 90 THEN 'High'
WHEN Value BETWEEN 90 AND 100 THEN 'Very High'
END
FROM Result.Calculation C
INNER JOIN Data.SampleSet S ON C.SampleSetID = S.ID
WHERE S.SampleDrawn >= DATEADD(MONTH,-3,GETDATE()) AND S.Department = 'LOCATION A'
Currently, I am using FromSql as below to call a stored procedure. I would like to know whether I can do the same without using stored procedures?
var result = context.MyData.FromSql("data.GetMyData #pType = {0}, #pLocation = {1}, #pNoOfDays = {2}, #pStartDate = {3}, #pEndDate = {4}", type, location, noOfDays, startDate, endDate).ToList();
Thanks.

You can use this.
from C in Calculations
join S in SampleSets on C.SampleSetID equals S.ID
where S.SampleDrawn >= DateTime.Now.AddMonths(-3)
&& S.Department == "LOCATION A"
select new {
All = 1
, Range =
(C.Value >= 0 && C.Value < 25) ? "Low" :
(C.Value >= 25 && C.Value < 75) ? "Medium" :
(C.Value >= 75 && C.Value < 90) ? "High" :
(C.Value >= 90 && C.Value <= 100) ? "Very High" : null
}

You can use this if it suits you.
I would just explain the LINQ query part.
You can use this with EF. I created dummy data for these.
For EF, use IQueryable instead.
// from a row in first table
// join a row in second table
// on a.Criteria equal to b.Criteria
// where additional conditions
// select the records into these two fields called All and Range
// Convert the result set to list.
var query = (from a in lstCalc
join b in lstSampleSet
on a.SampleSetID equals b.ID where b.SampleDrawn >= DateTime.Now.AddMonths(-8)
&& b.Department == "Location A"
select new { All = 0, Range = Utilities.RangeProvider(a.Value) }).ToList();
EDIT : LINQ Query for grouped result.. Make sure you are using IQueryable.
var query = (from a in lstCalc
join b in lstSampleSet
on a.SampleSetID equals b.ID where b.SampleDrawn >= DateTime.Now.AddMonths(-8)
&& b.Department == "Location A"
group a by Utilities.RangeProvider(a.Value) into groupedData
select new Result { All = groupedData.Sum(y => y.Value), Range =
groupedData.Key }).ToList();
Here is the code for the same.
public class Program
{
public static void Main(string[] args) {
List<Calculation> lstCalc = new List<Calculation>();
lstCalc.Add(new Calculation() {SampleSetID=1, Value=10 });
lstCalc.Add(new Calculation() { SampleSetID = 1, Value = 10 });
lstCalc.Add(new Calculation() { SampleSetID = 2, Value = 20 });
lstCalc.Add(new Calculation() { SampleSetID = 3, Value = 30 });
lstCalc.Add(new Calculation() { SampleSetID = 4, Value = 40 });
lstCalc.Add(new Calculation() { SampleSetID = 5, Value = 50 });
lstCalc.Add(new Calculation() { SampleSetID = 6, Value = 60 });
lstCalc.Add(new Calculation() { SampleSetID = 7, Value = 70 });
lstCalc.Add(new Calculation() { SampleSetID = 8, Value = 80 });
lstCalc.Add(new Calculation() { SampleSetID = 9, Value = 90 });
List<SampleSet> lstSampleSet = new List<SampleSet>();
lstSampleSet.Add(new SampleSet() {Department = "Location A", ID=1, SampleDrawn=DateTime.Now.AddMonths(-5)});
lstSampleSet.Add(new SampleSet() { Department = "Location A", ID = 2, SampleDrawn = DateTime.Now.AddMonths(-4) });
lstSampleSet.Add(new SampleSet() { Department = "Location A", ID = 3, SampleDrawn = DateTime.Now.AddMonths(-3) });
lstSampleSet.Add(new SampleSet() { Department = "Location A", ID = 4, SampleDrawn = DateTime.Now.AddMonths(-2) });
lstSampleSet.Add(new SampleSet() { Department = "Location A", ID = 5, SampleDrawn = DateTime.Now.AddMonths(-2) });
lstSampleSet.Add(new SampleSet() { Department = "Location A", ID = 6, SampleDrawn = DateTime.Now.AddMonths(-2) });
lstSampleSet.Add(new SampleSet() { Department = "Location A", ID = 7, SampleDrawn = DateTime.Now.AddMonths(-1) });
var query = (from a in lstCalc
join b in lstSampleSet
on a.SampleSetID equals b.ID where b.SampleDrawn >= DateTime.Now.AddMonths(-8)
&& b.Department == "Location A"
select new { All = 0, Range = Utilities.RangeProvider(a.Value) }).ToList();
Console.WriteLine(query.Count);
Console.ReadLine();
}
}
public class Utilities
{
public static string RangeProvider(int value)
{
if (value > 0 && value <= 25)
{ return "Low"; }
if (value > 25 && value <= 75)
{ return "Medium"; }
if (value > 75 && value <= 90)
{ return "High"; }
else
{ return "Very High"; }
}
}
public class Result {
public int All { get; set; }
public string Range { get; set; }
}
public class Calculation
{
public int SampleSetID { get; set; }
public int Value { get; set; }
}
public class SampleSet
{
public int ID { get; set; }
public DateTime SampleDrawn { get; set; }
public string Department { get; set; }
}

Below is the final LINQ statement which worked for me. As Amit explain in his answer RangeProvider method will be used to replace the SQL CASE statement.
var test2 = (from a in context.Calculations
join b in context.SampleSets on a.SampleSetID equals b.ID
where b.SampleDrawn >= DateTime.Now.AddDays(-10) && b.Department == "Location A"
group a by RangeProvider(a.Value) into groupedData
select new { All = groupedData.Count(), Range = groupedData.Key });

Related

Combine multiple lists into one list and order by amount

Combine multiple lists into one list and order by amount
I have a class like this
public class PriceList
{
public string Name { get; set; }
public int Amount { get; set; }
public int Price { get; set; }
}
First list. It's called VOne
Name Amount Price
P5 5000 6
P10 10000 10
P20 20000 20
Second list. It's called VTwo
Name Amount Price
P5 5000 5
P10 10000 10
P15 15000 15
P20 20000 21
Third list. It's called VThree
Name Amount Price
P1 1000 1
P10 10000 9
P20 20000 19
I want the result like this
Name Amount VOne VTwo VThree
P1 1000 0 0 1
P5 5000 6 5 0
P10 10000 10 10 9
P15 15000 0 15 0
P20 20000 20 21 19
This is what I try. It works but seems sophisticated. I need the simpler way to do this. Linq will be prefered but I have no idea how to use it.
static void Test()
{
var VOne = new List<PriceList>(new[]
{
new PriceList { Name = "P5", Amount = 5000, Price = 6},
new PriceList { Name = "P10", Amount = 10000 , Price = 10},
new PriceList { Name = "P20", Amount = 20000, Price = 20}
});
var VTwo = new List<PriceList>(new[]
{
new PriceList { Name = "P5", Amount = 5000, Price = 5},
new PriceList { Name = "P10", Amount = 10000 , Price = 10},
new PriceList { Name = "P15", Amount = 10000 , Price = 15},
new PriceList { Name = "P20", Amount = 20000, Price = 21}
});
var VThree = new List<PriceList>(new[]
{
new PriceList { Name = "P1", Amount = 5000, Price = 1},
new PriceList { Name = "P10", Amount = 10000 , Price = 9},
new PriceList { Name = "P20", Amount = 20000, Price = 19}
});
var prices = new List<PriceListResult>();
foreach (var m in VOne)
{
var exist = prices.Any(x => x.Name == m.Name && x.Amount == m.Amount);
if (!exist)
prices.Add(new PriceListResult { Name = m.Name, Amount = m.Amount });
}
foreach (var m in VTwo)
{
var exist = prices.Any(x => x.Name == m.Name && x.Amount == m.Amount);
if (!exist)
prices.Add(new PriceListResult { Name = m.Name, Amount = m.Amount });
}
foreach (var m in VThree)
{
var exist = prices.Any(x => x.Name == m.Name && x.Amount == m.Amount);
if (!exist)
prices.Add(new PriceListResult { Name = m.Name, Amount = m.Amount });
}
prices = prices.OrderBy(x => x.Name).ThenBy(x => x.Amount).ToList();
foreach (var price in prices)
{
var v1 = VOne.FirstOrDefault(x => x.Name == price.Name && x.Amount == price.Amount);
if (v1 != null)
price.VOne = v1.Price;
var v2 = VTwo.FirstOrDefault(x => x.Name == price.Name && x.Amount == price.Amount);
if (v2 != null)
price.VTwo = v2.Price;
var v3 = VThree.FirstOrDefault(x => x.Name == price.Name && x.Amount == price.Amount);
if (v3 != null)
price.VThree = v3.Price;
}
}
public class PriceListResult
{
public string Name { get; set; }
public int Amount { get; set; }
public int VOne { get; set; }
public int VTwo { get; set; }
public int VThree { get; set; }
}
My advice would be that you spend some time get acquainted to the standard LINQ methods
You can use Enumerable.Concat to put your three sequences into one sequence, then you can use one of the overloads of Enumerable.GroupBy to make groups of PriceLists that have the same amount. User parameter resultSelector to create one object per Amount and PriceLists with this Amount.
The problem is, that if you've got a PriceList, you can't see whether it is a PriceList of vOne, vTwo or vThree. So from every Price we'll have to remember the Amount, the Price and from which priceList the data originated.
I'll do it in small steps, so it is easy to see what is done. If you want, you can put it in one big LINQ. As all statements use delayed execution, this will not improve performance, however one big LINQ will deteriorate readability.
var vOne = VOne.Select(priceList => new
{
Id = 1,
Name = priceList.Name,
Amount = priceList.Amount,
Price = priceList.Price,
});
var vTwo = Select(priceList => new
{
Id = 2,
Name = priceList.Name,
Amount = priceList.Amount,
Price = priceList.Price,
});
vThree = Select(priceList => new
{
Id = 3,
Name = priceList.Name,
Amount = priceList.Amount,
Price = priceList.Price,
});
var allPriceLists = vOne.Concat(vTwo).Concat(vThree);
Make groups of sequences that have the same value for the combination [Name, Amount]. Use parameter resultSelector to create one object per group
var result = allPriceLists.GroupBy(priceList => new {priceList.Name, priceList.Amount},
// parameter resultSelector: use every [Name, Amount] combination,
// with all priceLists that have this combination to make one new:
(nameAmountCombinatin, priceListsWithThisCombination) => new
{
Name = nameAmountCombination.Name,
Amount = nameAmountCombination.Amount,
Vone = priceListsWithThisCombination
.Where(priceList => priceList.Id == 1)
.Select(priceList => priceList.Amount)
.FirstOrDefault(),
Vtwo = priceListsWithThisCombination
.Where(priceList => priceList.Id == 2)
.Select(priceList => priceList.Amount)
.FirstOrDefault(),
Vthree = priceListsWithThisCombination
.Where(priceList => priceList.Id == 3)
.Select(priceList => priceList.Amount)
.FirstOrDefault(),
})
Note: I assume that in Vone (etc.) doesn't have two PriceLists with the same [name, amount] combination. If you allow that Vone has two elements with [P5, 5000], consider to Sum the Amounts
If there is a missing Amount in one of the original PriceLists, you will get the default value for Price: 0.
make class like this:
public class PriceList
{
public string Name { get; set; }
public int Amount { get; set; }
public int Price { get; set; }
public string type {get;set;}
}
Then:
var list1Typed = List1.ForEach(f => f.type = "VOne");
var list2Typed = List2.ForEach(f => f.type = "VTwo");
var list3Typed = List3.ForEach(f => f.type = "VThree");
var all = list1Typed.Union(list2Typed).Union(list3Typed).ToList();
var allComplete = all.GroupBy(g => g.Name).Select(s =>
new PriceListResult(){
Name = g.Key,
Amount = g.First().Amount,
VOne = g.Any(a => a.Type == "VOne") ? g.Where(w => w.Type =="VOne").Sum(s => s.Amount) : 0,
VTwo = g.Any(a => a.Type == "VTwo ") ? g.Where(w => w.Type =="VTwo").Sum(s => s.Amount) : 0,
VThree = g.Any(a => a.Type == "VThree") ? g.Where(w => w.Type =="VThree").Sum(s => s.Amount) : 0
}).OrderBy(o => o.Amount).ToList();
Try this:
var all = list1.Union(list2).Union(list3).ToList();
then:
var grouped = all.GroupBy(p => new {
p.Name,
p.Amount
});
finally:
var results = grouped.Select(p => new {
p.Name,
p.Amount,
Price = string.Join(", ", p.Select(x => x.Price))
}).ToList().OrderBy(item => item.Amount);
var finalResult = results.Select(item => new PriceListResult {
Name = item.Name,
Amount = item.Amount ,
VOne = Price.Split(new[] { ','},StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray()[0],
VTwo = Price.Split(new[] { ','},StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray()[1],
VThree = Price.Split(new[] { ','},StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray()[2]
}).ToList();
I've edited Kai's answer and it works
var results = list1.Union(list2).Union(list3)
.GroupBy
(
p => p.Name,
p => p,
(key, list) => new { Name = key, Amount = list.First().Amount, List = list }
)
.Select(s => new
{
Name = s.Name,
Amount = s.Amount,
VOne = list1.Where(x => x.Name == s.Name && x.Amount == x.Amount).Select(x => x.Price).FirstOrDefault(),
VTwo = list2.Where(x => x.Name == s.Name && x.Amount == x.Amount).Select(x => x.Price).FirstOrDefault(),
VThree = list3.Where(x => x.Name == s.Name && x.Amount == x.Amount).Select(x => x.Price).FirstOrDefault()
})
.OrderBy(o => o.Amount).ToList();

LINQ Join usage data from two data sets into one

What I have:
Two lists of the following model:
int SubscriptionId
int ItemId
double Usage
double EffectiveRate
string ResourceName
string UnitOfMeasure
The first contains usage data of the last month like this:
SubscriptionId ItemId Usage EffectiveRate ResourceName UnitOfMesaure
_________________________________________________________________________
1 1 2 2,75 R1 U1
1 2 3 1,50 R2 U2
The seconds contains usage data of the current month like this:
SubscriptionId ItemId Usage EffectiveRate ResourceName UnitOfMesaure
_________________________________________________________________________
1 1 5 2,75 R1 U1
1 3 2 1,50 R3 U3
What I want:
This should be merge in a list like this:
SubscriptionId ItemId UsageThis UsageLast EffRate ResName UOM
_________________________________________________________________________
1 1 5 2 2,75 R1 U1
1 2 0 3 1,50 R2 U2
1 3 2 0 1,50 R3 U3
What I have:
//data for both months available
if (resourcesThisMonth.Any() && resourcesLastMonth.Any())
{
//join both months
resources = from resourceLastMonth in resourcesLastMonth
join resourceThisMonth in resourcesThisMonth
on new { resourceLastMonth.SubscriptionId, resourceLastMonth.ItemId } equals new { resourceThisMonth.SubscriptionId, resourceThisMonth.ItemId }
select new Resource
{
SubscriptionId = resourceThisMonth.SubscriptionId,
ItemId = resourceThisMonth.ItemId,
UsageThisMonth = resourceThisMonth.Usage,
UsageLastMonth = resourceLastMonth.Usage,
EffectiveRate = resourceThisMonth.EffectiveRate,
ResourceName = resourceThisMonth.ResourceName,
UnitOfMeasure = resourceThisMonth.UnitOfMeasure
};
//resources only last month available
var resourcesOnlyLastMonth = resourcesLastMonth.Where(r => !resourcesThisMonth.Where(s => s.ItemId == r.ItemId && s.SubscriptionId == r.SubscriptionId).Any())
.Select(r => new Resource
{
SubscriptionId = r.SubscriptionId,
ItemId = r.ItemId,
UsageThisMonth = 0.0,
UsageLastMonth = r.Units,
EffectiveRate = r.EffectiveRate,
ResourceName = r.ResourceName,
UnitOfMeasure = r.UnitOfMeasure
});
//resources only this month available
var resourcesOnlyThisMonth = resourcesThisMonth.Where(r => !resourcesLastMonth.Where(s => s.ItemId == r.ItemId && s.SubscriptionId == r.SubscriptionId).Any())
.Select(r => new Resource
{
SubscriptionId = r.SubscriptionId,
ItemId = r.ItemId,
UsageThisMonth = r.Usage,
UsageLastMonth = 0.0,
EffectiveRate = r.EffectiveRate,
ResourceName = r.ResourceName,
UnitOfMeasure = r.UnitOfMeasure
});
//union data
resources = resources.Union(resourcesOnlyLastMonth);
resources = resources.Union(resourcesOnlyThisMonth);
}
//data for last month available
else if (resourcesLastMonth.Any())
{
resources = from resource in resourcesLastMonth
select new Resource
{
SubscriptionId = resource.SubscriptionId,
ItemId = resource.ItemId,
UsageThisMonth = 0.0,
UsageLastMonth = resource.Usage,
EffectiveRate = resource.EffectiveRate,
ResourceName = resource.ResourceName,
UnitOfMeasure = resource.UnitOfMeasure
};
}
//data for this month available
else if (resourcesThisMonth.Any())
{
resources = from resource in resourcesThisMonth
select new Resource
{
SubscriptionId = resource.SubscriptionId,
ItemId = resource.ItemId,
UsageThisMonth = resource.Usage,
UsageLastMonth = 0.0,
EffectiveRate = resource.EffectiveRate,
ResourceName = resource.ResourceName,
UnitOfMeasure = resource.UnitOfMeasure
};
}
//no data available
else
{
resources = new List<Resource>();
}
Problem:
This is very much code - should be less, any possible solutions failed so far
Thanks for helping!
public class ExampleClass
{
public int Id1 { get; set; }
public int Id2 { get; set; }
public int Usage { get; set; }
public int UsageThis { get; set; }
public int UsageLast { get; set; }
}
List<ExampleClass> listThisMonth = new List<ExampleClass>
{
new ExampleClass{Id1=1, Id2=1,Usage=7, UsageThis=1, UsageLast=0},
new ExampleClass{Id1=2, Id2=2,Usage=4, UsageThis=2, UsageLast=0},
new ExampleClass{Id1=3, Id2=3,Usage=1, UsageThis=3, UsageLast=0},
};
List<ExampleClass> listLastMonth = new List<ExampleClass>
{
new ExampleClass{Id1=1, Id2=1,Usage=3, UsageThis=1, UsageLast=1},
new ExampleClass{Id1=4, Id2=4,Usage=3, UsageThis=4, UsageLast=3},
new ExampleClass{Id1=2, Id2=2,Usage=1, UsageThis=8, UsageLast=6},
};
var result = listThisMonth.Select(a=>new {value=a, list=1})
.Union(listLastMonth.Select(a => new { value = a, list = 2 }))
.GroupBy(a => new { Id1 = a.value.Id1, Id2 = a.value.Id2 })
.Select(x => new ExampleClass
{
Id1 = x.Key.Id1,
Id2 = x.Key.Id2,
UsageThis = x.Any(o => o.list == 1) ? x.First(o => o.list == 1).value.Usage : 0,
UsageLast = x.Any(o => o.list == 2) ? x.First(o => o.list == 2).value.Usage : 0,
Usage = x.Sum(o=>o.value.Usage)
}).ToList();
//id1 id2 current last sum
//1 1 7 3 10
//2 2 4 1 5
//3 3 1 0 1
//4 4 0 3 3
It looks to me that what you're looking for is a full outer join. Unfortunately, it looks like LINQ doesn't have a construct for that. So, there are a few options: LINQ - Full Outer Join
For your scenario, it looks like you have some redundant code. You should be able to do the union using two outer joins to get the correct result set. For example:
// Left join the current month with the last month
var currentMonth =
from current in resourcesThisMonth
join last in resourcesLastMonth on new { current.SubscriptionId, current.ItemId } equals new { last.SubscriptionId, last.ItemId } into outer
from o in outer.DefaultIfEmpty()
select new Resource
{
SubscriptionId = current.SubscriptionId,
ItemId = current.ItemId,
UnitsThisMonth = current.Units,
UnitsLastMonth = o?.Units ?? 0, // Replace NULL with 0
EffectiveRate = current.EffectiveRate,
ResourceName = current.ResourceName,
UnitOfMeasure = current.UnitOfMeasure
};
// Reverse of the first join. Last month LEFT JOIN Current month
var lastMonth =
from last in resourcesLastMonth
join current in resourcesThisMonth on new { last.SubscriptionId, last.ItemId } equals new { current.SubscriptionId, current.ItemId } into outer
from o in outer.DefaultIfEmpty()
select new Resource
{
SubscriptionId = last.SubscriptionId,
ItemId = last.ItemId,
UnitsThisMonth = o?.Units ?? 0, // Replace NULL with 0
UnitsLastMonth = last.Units,
EffectiveRate = o?.EffectiveRate ?? last.EffectiveRate,
ResourceName = o?.ResourceName ?? last.ResourceName,
UnitOfMeasure = o?.UnitOfMeasure ?? last.UnitOfMeasure
};
// Union them together to get a full join
var resources = currentMonth.Union(lastMonth);

Find least Rate in a List<T> in a specific group

I have List<Rates> finalReportDetails and it contains multiple rates for same WebsiteId and CheckInDate.
I need to have only one record for each websiteId and checkindate.
This record either should have the lowest Rate (1st preference) or with Rate -1.
Rest all records for this group should be removed from the list.
Initial List
List<Rates> rates = new List<Rates>()
{
new Rates { CheckInDate = timeValue, websiteId = 1, price = 1 },
new Rates { CheckInDate = timeValue, websiteId = 1, price = 2 },
new Rates { CheckInDate = timeValue, websiteId = 2, price = -1 },
new Rates { CheckInDate = timeValue, websiteId = 2, price = 2 },
new Rates { CheckInDate = timeValue, websiteId = 3, price = -1 },
new Rates { CheckInDate = timeValue, websiteId = 3, price = -1 },
};
Final List
List<Rates> rates = new List<Rates>()
{
new Rates { CheckInDate = timeValue, websiteId = 1, price = 1 },
new Rates { CheckInDate = timeValue, websiteId = 2, price = 2 },
new Rates { CheckInDate = timeValue, websiteId = 3, price = -1 },
};
I have tried this code, but going through the loop takes a lot of time.
First I have found distinct groups by by CheckInDate, WebsiteId.
Then for each group, I am checking for the required rate.
class Rates {
public int websiteId {get; set;},
public DateTime CheckInDate {get; set;}
public decimal price {get; set;}}
var grouped = (from s in finalReportDetails
select new { s.CheckInDate,s.websiteId })
.Distinct()
.ToList();
for (int i = 1; i <= grouped.Count && finalReportDetails.Count != grouped.Count; i++)
{
var obj = grouped[i - 1];
// Fetch records for one group, order by rate to find the least Rate
var grpFinalReportDetails = (from s in Rates
where && s.CheckInDate == obj.CheckInDate && s.websiteId == obj.websiteId
select s).OrderBy(x => x.price).ToList();
// Deletion necessary only if there is more than one rate for same parameters
if (grpFinalReportDetails.Count > 1)
{
// Tracks if a valid rate is found
bool isFound = false;
for (int j = 0; j < grpFinalReportDetails.Count; j++)
{
// Checks if a valid least rate is found
if (!isFound && grpFinalReportDetails[j].InitialRates.Rates > 0)
{
isFound = true;
continue;
}
// Delete all but one records whose Rate is less than 0 OR whose rate is more than the cheapest rate
if ((grpFinalReportDetails[j].InitialRates.Rates <= 0 && j < grpFinalReportDetails.Count - 1) || isFound)
{
finalReportDetails.Remove(grpFinalReportDetails[j]);
}
}
}
}
Is there any faster way to find the same using linq?
Or something that can be optmized in this code.
It seems like this LINQ query might do what you want - at least, it passes your example:
var result = rates
.GroupBy(rate => rate.websiteId)
.Select(#group =>
#group.Any(rate => rate.price > 0)
? #group.Where(rate => rate.price > 0).OrderBy(rate => rate.price).First()
: #group.OrderBy(rate => rate.price).First())
(The # sign in the variable name #group is because group is a reserved word. If you choose a different variable name, you don't need #.)
Note that this might iterate over your enumerable several times, so if this is a list coming from some expensive operation (like a database query), be sure to call .ToList() first, to avoid invoking the expensive operation more than once.
//Some initializing code for testing
var timeValue = DateTime.Now;
List<Rates> rates = new List<Rates>()
{
new Rates { CheckInDate = timeValue, websiteId = 1, price = 1 },
new Rates { CheckInDate = timeValue, websiteId = 1, price = 2 },
new Rates { CheckInDate = timeValue, websiteId = 2, price = -1 },
new Rates { CheckInDate = timeValue, websiteId = 2, price = 2 },
new Rates { CheckInDate = timeValue, websiteId = 3, price = -1 },
new Rates { CheckInDate = timeValue, websiteId = 3, price = -1 },
};
//The actual relevant code
var result = rates.GroupBy(item => new { item.websiteId, item.CheckInDate })
.Select(grp => grp.Any(item => item.price != -1) ?
grp.Where(item => item.price != -1).OrderBy(item => item.price).First() :
grp.First())
.ToList();

Multicolumn LIST<T> - Finding duplicates of a one column based on another column

I need to find the duplicate items of one column (Qty) based on another column (Priority). I have a List that contains the following data:
Priority Product Qty
0 a 10
0 b 20
1 c 50
1 d 20
1 e 50
1 f 10
1 g 20
1 h 10
I need to produce a sorted List<T> that contains only the duplicates in terms of Qty among the items with priority 0.
I.e. the resulting List<T> would contain:
Priority Product Qty
0 a 10
1 f 10
1 h 10
0 b 20
1 d 20
1 g 20
Is there a simple LINQ/Lambda expression for doing this?
Here is a solution with GroupBy:
var result = input
.GroupBy(p => p.Qty)
.Where(g => g.Any(p0 => p0.Priority == 0))
.Where(g => g.Skip(1).Any())
.SelectMany(g => g)
.OrderBy(g => g.Qty);
Try this:
static void Main(string[] args)
{
var items = new List<Item>
{
new Item { Priority = 0, Product = "a", Qty = 10 },
new Item { Priority = 0, Product = "b", Qty = 20 },
new Item { Priority = 1, Product = "c", Qty = 50 },
new Item { Priority = 1, Product = "d", Qty = 20 },
new Item { Priority = 1, Product = "e", Qty = 50 },
new Item { Priority = 1, Product = "f", Qty = 10 },
new Item { Priority = 1, Product = "g", Qty = 20 },
new Item { Priority = 1, Product = "h", Qty = 10 }
};
foreach (var group in items.Where (i => i.Priority == 0)
.GroupBy(i => i, g => items
.Where (t => t.Qty == g.Qty &&
t.Product != g.Product)))
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(group.Key); // Priority == 0
Console.ForegroundColor = ConsoleColor.Gray;
foreach (var item in group.SelectMany(i => i)) // dups
Console.WriteLine("\t{0}", item);
}
}
class Item
{
public int Priority { get; set; }
public string Product { get; set; }
public int Qty { get; set; }
public override string ToString()
{
return String.Format("{0}\t{1}\t{2}",
this.Priority, this.Product, this.Qty);
}
}
Assuming you have an enumerable container called list that contains elements with properties Priority, Product and Qty:
var orderedResult = list.Where(element => !list.Contains(x => x != element && x.Priority == element.Priority && x.Qty == element.Qty)).OrderBy(element => element.Qty).ThenBy(element => element.Priority);

Group by multiple values and break down to weekly periods

I'm still learning LINQ and have a task where I need to group Booking objects by four properties and then by weekly intervals depending on the input timerange.
public class Booking
{
public string Group { get; set; }
public BookingType Type { get; set; }
public BookingStatus Status { get; set; }
public decimal Price { get; set; }
public bool Notification { get; set; }
public DateTime Date { get; set; }
}
Let's say we have the following Bookings:
IList<Booking> Bookings = new List<Booking>
{
new Booking{Group = "Group1", Type = BookingType.Online, Status = BookingStatus.New, Price = 150, Date = new DateTime(2012,06,01)},
new Booking{Group = "Group1", Type = BookingType.Online, Status = BookingStatus.New, Price = 100, Date = new DateTime(2012,06,02)},
new Booking{Group = "Group1", Type = BookingType.Online, Status = BookingStatus.New, Price = 200, Date = new DateTime(2012,06,03)},
new Booking{Group = "Group2", Type = BookingType.Phone, Status = BookingStatus.Accepted, Price = 80, Date = new DateTime(2012,06,10)},
new Booking{Group = "Group2", Type = BookingType.Phone, Status = BookingStatus.Accepted, Price = 110, Date = new DateTime(2012,06,12)},
new Booking{Group = "Group3", Type = BookingType.Store, Status = BookingStatus.Accepted, Price = 225, Date = new DateTime(2012,06,20)},
new Booking{Group = "Group3", Type = BookingType.Store, Status = BookingStatus.Invoiced, Price = 300, Date = new DateTime(2012,06,21)},
new Booking{Group = "Group3", Type = BookingType.Store, Status = BookingStatus.Invoiced, Price = 140, Date = new DateTime(2012,06,22)},
};
That would result in the following lines on the final printout:
Week 22 Week 23 Week 24 Week 25 Week 26
May28-Jun3 Jun4-10 Jun11-17 Jun18-24 Jun25-Jul1
Group Type Status Cnt. Price Cnt. Price Cnt. Price Cnt. Price Cnt. Price
----------------------------------------------------------------------------------------------
Group1 Online New 3 450 0 0 0 0 0 0 0 0
Group2 Phone Accepted 0 0 1 80 1 110 0 0 0 0
Group3 Store Accepted 0 0 0 0 0 0 1 225 0 0
Group3 Store Invoiced 0 0 0 0 0 0 2 440 0 0
I have created 2 additional classes to represent a line with the possibility to include any number of weeks:
public class GroupedLine
{
public string Group { get; set; }
public BookingType Type { get; set; }
public BookingStatus Status { get; set; }
public List<WeeklyStat> WeeklyStats { get; set; }
}
public class WeeklyStat
{
public DateTime WeekStart { get; set; }
public decimal Sum { get; set; }
public int Count { get; set; }
}
If I have the following time period:
var DateFrom = new DateTime(2012, 05, 28);
var DateTo = new DateTime(2012, 7, 01);
Firstly, I need to identify what weeks are necessary in the statistics: in this case week 22-26.
For that I have the following code:
var DateFrom = new DateTime(2012, 05, 28);
var DateTo = new DateTime(2012, 7, 01);
var firstWeek = GetFirstDateOfWeek(DateFrom, DayOfWeek.Monday);
IList<DateTime> weeks = new List<DateTime> { firstWeek };
while(weeks.OrderByDescending(w => w).FirstOrDefault().AddDays(7) <= DateTo)
{
weeks.Add(weeks.OrderByDescending(w => w).FirstOrDefault().AddDays(7));
}
And now, I'd need some LINQ magic to do the grouping both by the 4 properties and the aggregation (count of bookings and sum of prices) for the weeks.
I can attach code sample of the LINQ I got so far tomorrow, as I don't have access to it now.
Sorry for the long post, hope it's clear what I mean.
Edit: 2012-11-07
I have to modify the question a bit, so that the grouped weeks only include those weeks, that actually have data.
Updated example output:
May28-Jun3 Jun4-10 Jun18-24
Group Type Status Cnt. Price Cnt. Price Cnt. Price
---------------------------------------------------------------
Group1 Online New 3 450 0 0 0 0
Group2 Phone Accepted 0 0 1 80 0 0
Group3 Store Accepted 0 0 0 0 1 225
Group3 Store Invoiced 0 0 0 0 2 440
In this case there were no Bookings in period Jun 11-17 and Jun25-Jul1 so they are omitted from the results.
This query will get all data
var query = from b in Bookings
where b.Date >= dateFrom && b.Date <= dateTo
group b by new { b.Group, b.Type, b.Status } into g
select new GroupedLine()
{
Group = g.Key.Group,
Type = g.Key.Type,
Status = g.Key.Status,
WeeklyStats = (from b in g
let startOfWeek = GetFirstDateOfWeek(b.Date)
group b by startOfWeek into weekGroup
orderby weekGroup.Key
select new WeeklyStat()
{
WeekStart = weekGroup.Key,
Count = weekGroup.Count(),
Sum = weekGroup.Sum(x => x.Price)
}).ToList()
};
I leave UI output to you :)
This will also return WeekStats for all weeks (with 0 values, if we do not have booking groups on some week):
// sequence contains start dates of all weeks
var weeks = Bookings.Select(b => GetFirstDateOfWeek(b.Date))
.Distinct().OrderBy(date => date);
var query = from b in Bookings
group b by new { b.Group, b.Type, b.Status } into bookingGroup
select new GroupedLine()
{
Group = bookingGroup.Key.Group,
Type = bookingGroup.Key.Type,
Status = bookingGroup.Key.Status,
WeeklyStats = (from w in weeks
join bg in bookingGroup
on w equals GetFirstDateOfWeek(bg.Date) into weekGroup
orderby w
select new WeeklyStat()
{
WeekStart = w,
Count = weekGroup.Count(),
Sum = weekGroup.Sum(b => b.Price)
}).ToList()
};
Keep in mind, that if you need date filter (from, to), then you need to apply it both to weeks query and bookings query.
var query = Bookings.GroupBy(book => new GroupedLine()
{
Group = book.Group,
Type = book.Type,
Status = book.Status
})
.Select(group => new
{
Line = group.Key,
Dates = group.GroupBy(book => GetWeekOfYear(book.Date))
.Select(innerGroup => new
{
Week = innerGroup.Key,
Count = innerGroup.Count(),
TotalPrice = innerGroup.Sum(book => book.Price)
})
});
public static int GetWeekOfYear(DateTime date)
{
return date.DayOfYear % 7;
}
This variant generates "empty" spots for weeks with no data:
// I group by week number, it seemed clearer to me , but you can change it
var firstWeek = cal.GetWeekOfYear(DateFrom, dfi.CalendarWeekRule, dfi.FirstDayOfWeek);
var lastWeek = cal.GetWeekOfYear(DateTo, dfi.CalendarWeekRule, dfi.FirstDayOfWeek);
var q = from b in Bookings
group b by new { b.Group, b.Type, b.Status }
into g
select new
{
Group = g.Key,
Weeks =
from x in g
select new
{
Week = cal.GetWeekOfYear(x.Date,
dfi.CalendarWeekRule,
dfi.FirstDayOfWeek),
Item = x
}
} into gw
from w in Enumerable.Range(firstWeek, lastWeek-firstWeek+1)
select new
{
gw.Group,
Week = w,
Item = from we in gw.Weeks
where we.Week == w
group we by we.Item.Group into p
select new
{
p.Key,
Sum = p.Sum (x => x.Item.Price),
Count = p.Count()
}
};

Categories

Resources