Can this data be retrieved with a single linq query? - c#

I'm using Entity Framework. The class below represents a table from the database.
public partial class TermBucket
{
public short ID { get; set; }
public byte Period { get; set; }
public byte Min { get; set; }
public byte Max { get; set; }
}
The PK is ID and Period, so there can be multiple ID's in the table.
When querying the entity, I have at my disposal the period + time (the number associated with the period). Period is set up as an enum, so I would use it as:
Time: 3
Period: 3 (Days)
What I want to do is to find the bucket that matches by requirements and then get all entries for that bucket. Here's what I currently do:
Step 1: Get the ID
return r.Find() // this is my repository
.AsNoTracking()
.Where(x => (int)tp.Period == x.Period && tp.Time >= x.Min && tp.Time <= x.Max)
.Select(x => x.ID)
.Single();
Step 2: Get all entries for the ID, using the retrieved ID
return r.Find()
.AsNoTracking()
.Where(x => x.ID == ID );
So, there are 2 distinct queries, but is it possible to retrieve this data in one go?

Can't you just combine them?
return r.Find()
.AsNoTracking()
.Where(x => x.ID == r.Find()
.AsNoTracking()
.Where(x => (int)tp.Period == x.Period && tp.Time >= x.Min && tp.Time <= x.Max)
.Select(x => x.ID)
.Single());

You can do it using join. Example
public partial class TermBucket
{
public short ID { get; set; }
public byte Period { get; set; }
public byte Min { get; set; }
public byte Max { get; set; }
}
static void Main(string[] args)
{
List<TermBucket> l = new List<TermBucket>();
l.Add(new TermBucket() { ID = 1, Period = 3, Min = 10, Max = 14 });
l.Add(new TermBucket() { ID = 1, Period = 4, Min = 10, Max = 13 });
l.Add(new TermBucket() { ID = 1, Period = 5, Min = 100, Max = 25 });
l.Add(new TermBucket() { ID = -1, Period = 3, Min = 10, Max = 12 });
int period = 3;
int minV = 10;
int maxV = 13;
var res = from e in l
join e2 in l on e.ID equals e2.ID
where e.Period == period && minV >= e.Min && maxV <= e.Max
select e2;
foreach (var r in res)
{
Console.WriteLine(r.ID + " " + r.Period);
}
Console.ReadLine();
}
Will output
1 3
1 4
1 5

Yes, it is:
return r.Find()
.AsNoTracking()
.Where(x => x.ID == r.Find()
.AsNoTracking()
.Where(x => (int)tp.Period == x.Period && tp.Time >= x.Min && tp.Time <= x.Max)
.Select(x => x.ID)
.Single());
But I would recommend breaking it up into two queries, as you already have it, to handle the case where the first query returns no results. (As it is, currently, .Single() will throw an exception if .Select() is empty).

Related

EF core query group by date and subquery

I have the following table:
CREATE TABLE "OrderStatusLogs" (
"Id" UNIQUEIDENTIFIER NOT NULL,
"OrderId" UNIQUEIDENTIFIER NOT NULL,
"Status" INT NOT NULL,
"StartDateTime" DATETIMEOFFSET NOT NULL,
"EndDateTime" DATETIMEOFFSET NULL DEFAULT NULL,
PRIMARY KEY ("Id"),
FOREIGN KEY INDEX "FK_OrderStatusLogs_Orders_OrderId" ("OrderId"),
CONSTRAINT "FK_OrderStatusLogs_Orders_OrderId" FOREIGN KEY ("OrderId") REFERENCES "Orders" ("Id") ON UPDATE NO_ACTION ON DELETE CASCADE
)
;
For the following entity:
[DebuggerDisplay(nameof(OrderStatusLog) + " {Status} {StartDateTime} - {EndDateTime}" )]
public class OrderStatusLog
{
public Guid Id { get; set; }
public Guid OrderId { get; set; }
public OrderStatus Status { get; set; }
public DateTimeOffset StartDateTime { get; set; }
public DateTimeOffset? EndDateTime { get; set; }
}
public enum OrderStatus
{
Unknown = 0,
Pending = 1,
Processing = 2,
Shipping = 3,
}
And i'm trying to generate a report which should show how many orders are set to a certain state for a given range.
For example, for the month oktober, we'd have the range 1 to 31 oktober.
The desired output would be something like this:
1/10/2021 Pending 21 orders
1/10/2021 Processing 23 orders
1/10/2021 Shipping 33 orders
1/10/2021 Unknown 0 orders
...
31/10/2021 Pending 1 orders
31/10/2021 Processing 3 orders
31/10/2021 Shipping 44 orders
31/10/2021 Unknown 5 orders
I'm having some difficulties writing a query in EF that would give me the right output. I can get things to work, but only client-side. I'm trying to make this work in the database instead.
So far i tried:
var logsByDayAndOrderId = orderStatusLogs.GroupBy(c => new { c.StartDateTime.Date, c.OrderId }, (key, values) => new
{
key.Date,
key.OrderId,
MaxStartDateTime = values.Max(x => x.StartDateTime)
});
var list = logsByDayAndOrderId.ToList();
var statusByDayAndOrderId = logsByDayAndOrderId.Select(c => new
{
c.Date,
c.OrderId,
orderStatusLogs.FirstOrDefault(x => x.StartDateTime == c.MaxStartDateTime && x.OrderId == c.OrderId).Status
});
//var statusByDayAndOrderId = logsByDayAndOrderId.Join(orderStatusLogs.def, inner => new { inner.OrderId, StartDateTime = inner.MaxStartDateTime }, outer => new { outer.OrderId, outer.StartDateTime }, (inner,outer) => new
//{
// inner.Date,
// inner.OrderId,
// outer.Status
//}); // TODO rem this query gives more results because of the join. we need an Outer join - but i could not get that to work. the version with select above works better, but then it does not use join so it may be slow(er).
var list1 = statusByDayAndOrderId.ToList();
var groupBy = statusByDayAndOrderId
.GroupBy(c => new { c.Date, c.Status })
.Select(c => new { c.Key.Date, c.Key.Status, Count = c.Count() });
var list2 = groupBy.ToList();
Another attempt:
var datesAndOrders = orderStatusLogs
.GroupBy(c => new { c.StartDateTime.Date, c.OrderId }, (key, values) => key);
var ordersByDateAndActiveStatusLog = orderStatusLogs
.Select(c => new
{
c.StartDateTime.Date,
c.OrderId,
ActiveStatusForDate = orderStatusLogs
.OrderByDescending(x => x.StartDateTime)
.FirstOrDefault(x => x.OrderId == c.OrderId && x.StartDateTime.Date == c.StartDateTime.Date)
.Status
});
var list = ordersByDateAndActiveStatusLog.ToList();
var orderCountByDateAndStatus = ordersByDateAndActiveStatusLog
.GroupBy(c => new { c.Date, c.ActiveStatusForDate }, (key, values) => new
{
key, count = values.Count()
});
var list1 = orderCountByDateAndStatus.ToList();
Both of these fail because of Cannot use an aggregate or a subquery in an expression used for the group by list of a GROUP BY clause..
This makes sense.
I'm hoping for someone that could help write a Linq query that generates the right data using ef core.
Notes:
I Solely use the fluent query syntax
I Have more places where i'd like to get data for each day so any other info or tips and tricks are welcome
I use net core 5 with ef core 5.0.11 with a MSSQL database
I would suggest to use EF Core extension linq2db.EntityFrameworkCore which has ability to work with local (in-memory) collections in database queries. Disclaimer: i'm one of the creators.
At first define function which generates days sequence:
public static IEnumerable<DateTime> GenerateDays(int year, int month)
{
var start = new DateTime(year, month, 1);
var endDate = start.AddMonths(1);
while (start < endDate)
{
yield return start;
start = start.AddDays(1);
}
}
Then we can use generated sequence in LINQ Query:
var days = GenerateDays(2021, 10).ToArray();
using var dc = ctx.CreateLinqToDbConnection();
var totalsQuery =
from d in days.AsQueryable(dc)
from l in orderStatusLogs.Where(l =>
(l.EndDateTime == null || l.EndDateTime >= d) && l.StartDateTime < d.AddDays(1))
.DefaultIfEmpty()
group l by new { Date = d, l.Status } into g
into g
select new
{
g.Key.Date,
g.Key.Status,
Count = g.Sum(x => x == null ? 0 : 1),
};
var result = totalsQuery.ToList();
The following SQL should be generated:
SELECT
[d].[item],
[e].[Status],
Sum(IIF([e].[OrderID] IS NULL, 0, 1))
FROM
(VALUES
('2021-05-01T00:00:00'), ('2021-05-02T00:00:00'),
('2021-05-03T00:00:00'), ('2021-05-04T00:00:00'),
('2021-05-05T00:00:00'), ('2021-05-06T00:00:00'),
('2021-05-07T00:00:00'), ('2021-05-08T00:00:00'),
('2021-05-09T00:00:00'), ('2021-05-10T00:00:00'),
('2021-05-11T00:00:00'), ('2021-05-12T00:00:00'),
('2021-05-13T00:00:00'), ('2021-05-14T00:00:00'),
('2021-05-15T00:00:00'), ('2021-05-16T00:00:00'),
('2021-05-17T00:00:00'), ('2021-05-18T00:00:00'),
('2021-05-19T00:00:00'), ('2021-05-20T00:00:00'),
('2021-05-21T00:00:00'), ('2021-05-22T00:00:00'),
('2021-05-23T00:00:00'), ('2021-05-24T00:00:00'),
('2021-05-25T00:00:00'), ('2021-05-26T00:00:00'),
('2021-05-27T00:00:00'), ('2021-05-28T00:00:00'),
('2021-05-29T00:00:00'), ('2021-05-30T00:00:00'),
('2021-05-31T00:00:00')
) [d]([item])
LEFT JOIN [OrderStatusLogs] [e] ON ([e].[EndDateTime] IS NULL OR [e].[EndDateTime] >= [d].[item]) AND [e].[StartDateTime] < DateAdd(day, 1, [d].[item])
GROUP BY
[d].[item],
[e].[Status]

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();

Increment outer loop from inner loop in C# using Foreach

I am trying to search through a group of days, and determine if a worker has worked that day, and get a total of days worked. The below works, but is terribly inefficient since even after it finds a guy worked a day it keeps looking through the rest of those days. If I could somehow increment the outer ForEach loop when the inner condition (day worked) is satisfied it would surely be faster.
totalDaysWorked is what I'm after below:
public class StationSupportRequest
{
public string RequestNum;
public string Status;
public string Prefix;
public string PlantLoc;
public DateTime Date;
public string Departmnt;
public DateTime Time;
public string StationID;
public string Fixture;
public string Supervisor;
public string PartNo;
public string SerialNum;
public string FailedStep;
public string Reason;
public string OtherReason;
public string Details;
public string Urgency;
public DateTime Date_1;
public DateTime Time_1;
public DateTime Date_2;
public DateTime Time_2;
public string ProblemFound;
public string SolutionCode;
public string Solution;
public double ServiceTechHrs;
public double ServiceEngHrs;
public string DocHistory;
public DateTime CloseDate;
public DateTime IniDate;
public DateTime IniTime;
public string MOT;
public string Initiator;
public string Notification;
public string ServiceTech;
public string ServiceEng;
public string SolutionCode_1;
public string Solution_1;
public string UpdatedBy;
public List<string> UpdatedByList;
public string Revisions;
public List<DateTime> RevisionsDateTime;
public List<WorkedDatapoint> WorkedDataPointsList;
}
public class WorkedDatapoint
{
public string AssignerName { get; set; }
public string AssigneeName { get; set; }
public DateTime Date { get; set; }
public bool AssignedToOther { get; set; }
}
var DateRange = SSRList.Where(y => y.IniDate >= IniDate && y.CloseDate < EndDate);
//DateRange = DateRange.Where(dr => dr.Fixture != null && dr.Fixture.Length == 6); //To get valid fixtures if pivoting on "Fixture"
var groupedData = DateRange.GroupBy(x => new { DS = x.ServiceTech }).Select(x =>
{
double totalSsrsWorkedOn = x.Select(y => y.RequestNum).Count();
IEnumerable<TimeSpan> hoursWorked = x.Select(y => y.CloseDate - y.IniDate.AddDays(GetWeekendDaysToSubtract(y.IniDate, y.CloseDate)));
var averageReactionTimeMinutes = x.Where(d => d.IniDate != null && d.Revisions != null)
.Average(d => ((DateTime.Parse(d.Revisions.Split(',')[0]) - (DateTime)d.IniDate)).Minutes);
double[] listOfMinutesOpenTime = x.Where(d => d.IniDate != null && d.Revisions != null)
.Select(d => Convert.ToDouble(((DateTime.Parse(d.Revisions.Split(',')[0]) - (DateTime)d.IniDate)).Minutes))
.ToArray();
double[] listOfDaysOpenTime = x.Where(d => d.IniDate != null && d.CloseDate != null)
.Select(d => ((DateTime)d.CloseDate - (DateTime)d.IniDate.AddDays(GetWeekendDaysToSubtract(d.IniDate, d.CloseDate))).TotalDays)
.ToArray();
string testtech = x.Select(y => y.ServiceTech).FirstOrDefault();
List<DateTime> totalDaysInDateRange = Enumerable.Range(0, 1 + EndDate.Subtract(IniDate).Days)
.Select(offset => IniDate.AddDays(offset)).ToList();
double totalHoursLogged = x.Sum(d => d.ServiceEngHrs) + x.Sum(d => d.ServiceTechHrs);
int assignedToOthersCount = x.SelectMany(y => y.WorkedDataPointsList)
.Where(z => z.AssignerName.Contains(testtech) && z.AssignedToOther == true)
.Count();
int brokenWiresFixed = x.Where(d => d.SolutionCode != null)
.Where(d => d.SolutionCode.Contains("A01 -") ||
d.SolutionCode.Contains("F01 -") ||
d.SolutionCode.Contains("S01 -")).Count();
int npfResults = x.Where(d => d.ProblemFound != null).Where(d => d.ProblemFound.Contains("NPF")).Count();
int totalDaysWorked = 0;
List<DateTime> workingDatesList = new List<DateTime>();
totalDaysInDateRange.ForEach((day) =>
{
x.Select(y => y.WorkedDataPointsList).ForEach((WorkedDataPoint) =>
{
IEnumerable<WorkedDatapoint> dateList = WorkedDataPoint
.Where(y => testtech == y.AssignerName)
.DistinctBy(z => z.Date.Date);
foreach ( WorkedDatapoint date in dateList)
{
if (x.Any(b => b.Date.Date.Date == date.Date.Date.Date))
{
workingDatesList.Add(date.Date.Date.Date);
break;
}
}
});
});
workingDatesList.Dump("WorkingDatesList");
totalDaysWorked = workingDatesList.DistinctBy(b => b.Date).Count();
/*int totalDaysWorked = 0;
totalDaysInDateRange.ForEach((day) =>
{
if (AssignersList.Where(d => testtech.Contains(d.AssignerName))
.DistinctBy(d => d.Date.Date)
.Any(d => d.Date.Date == day.Date))
{
totalDaysWorked++;
}
}); TODO: Delete this once new is working*/
return new
{
//SSRs = x,
//Station = x.Select(d => d.StationID).FirstOrDefault(),
//Fixture = x.Select(d => d.Fixture).FirstOrDefault(),
//ProductTested = x.Select(d => d.Details).FirstOrDefault(),
TestTech = testtech,
//TestEng = x.Select(d => d.ServiceEng).Distinct().Where(d => d.Length > 0),
TotalSSRsWorkedOn = Math.Round(totalSsrsWorkedOn, 4),
TotalHoursLogged = Math.Round(totalHoursLogged, 4),
AssignedToOthersCount = assignedToOthersCount,
AssignedToOthersPercentage = 100 * Math.Round(assignedToOthersCount / (assignedToOthersCount + totalSsrsWorkedOn), 4),
//AverageReactionTimeMinutes = averageReactionTimeMinutes,
AverageTimeToCompleteHours = x.Where(y => y.CloseDate != null && y.Time_1 != null && y.Time_1 != DateTime.MinValue).Select(z => (z.CloseDate - z.Time_1).TotalHours).Average(),
//Close = x.Where(y => y.CloseDate != null && y.Time_1 != null).Select(z => (z.CloseDate)),
//Time = x.Where(y => y.CloseDate != null && y.Time_1 != null).Select(z => (z.Time_1)),
MedianDaysRequestOpen = Math.Round(GetMedian(listOfDaysOpenTime), 3),
DaysWorkedPerDateRange = totalDaysWorked,
AveSSRsClosedPerWorkedDay = Math.Round(totalSsrsWorkedOn / totalDaysWorked, 3),
AveHoursLoggedPerRequest = Math.Round((x.Select(y => y.ServiceTechHrs + y.ServiceEngHrs).Sum()) / totalSsrsWorkedOn, 3),
BrokenWiresFixed = brokenWiresFixed,
PercentageBrokenWires = 100 * Math.Round(brokenWiresFixed / totalSsrsWorkedOn, 4),
NPFResults = npfResults,
PercentageNPF = 100 * Math.Round(npfResults / totalSsrsWorkedOn, 4),
};
}).OrderByDescending(x => x.TotalSSRsWorkedOn)
.Dump("Summary");
return;
Sample output, with the duplicate dates evaluated (workingDatesList):
8/1/2017 12:00:00 AM
8/1/2017 12:00:00 AM
8/1/2017 12:00:00 AM
8/2/2017 12:00:00 AM
A couple of comments on the code you posted:
Since you don't ever use the day variable from the outermost loop, simply remove that loop altogether.
Why are you testing whether x.Any(...) within a loop that iterates over y? This seems fundamentally flawed.
I can't discern from your problem statement what your data structures are, nor what it is that you are actually trying to do. Your problem statement is currently worded as:
I am trying to search through a group of days, and determine if a worker has worked that day, and get a total of days worked.
It appears you are taking some input called testtech (String) and totalDaysInDateRange (List<DateTime>), then want to find all entries in some data structure x (I can't infer what this is) where String.equalsIgnoreCase(y.AssignerName, testtech) && totalDaysInDateRange.contains(y.Date). Is this interpretation correct?
If so, simply iterate over the entries in whatever your x data structure is, and run the above logic. If this doesn't solve your problem, then please give us more information on the layout of the data structure x and how information about each worker is actually associated with the other data about that worker.
BEGIN EDIT
OK, now that you have provided more information, I think you want to replace the totalDaysInDateRange.ForEach statement with the following:
x.Select(y => y.WorkedDataPointsList).ForEach((wdp) =>
{
if (testtech == wdp.AssignerName && IniDate.Date <= wdp.Date.Date
&& wdp.Date.Date <= EndDate.Date)
{
workingDatesList.Add(wdp.Date.Date);
}
});
After changing your implementation, simply delete totalDaysInDateRange. I also recommend changing the type of workingDatesList to HashSet<DateTime>, since you don't seem to care about duplicate dates. Be sure to convert workingDatesList to a list and sort it once the loop is complete if you want the dates printed in chronological order.

Using SUM & GROUP BY on Collection in Linq

I want to use SUM & GOUP BY in linq. I have records in a collection & want to use the below query to execute on the collection by linq
public class Summary
{
public int Month { get; set; }
public int Year { get; set; }
public double Wages { get; set; }
public double AccNo22 { get; set; }
public double AccNo21 { get; set; }
}
List<Summary> list = new List<Summary>();
for (int i = 0; i < data.Rows.Count; i++)
{
Summary model = new Summary();
model.Month = int.Parse(data.Rows[i]["MonthNumber"].ToString());
model.Year = int.Parse(data.Rows[i]["Year"].ToString());
model.AccNo22 = 0;
model.AccNo21 = 0;
list.Add(model);
}
Query :-
SELECT Year, Month, SUM(ACCNO21) AS ACC21,SUM(ACCNO22) AS ACC22 FROM AboveList
WHERE (((Month >= 3 AND Month <= 12) AND Year = '2015') OR ((Month >= 1 AND Month <= 2) AND Year = '2016'))
GROUP BY Month,Year")
I am trying :-
var newCollection = list
.GroupBy(a => a.Month, b => b.Year)
.Select(a => new { Wages = a.Sum(a.Wages)})
.ToList();
Well, you can filter on list first, then group by and select elements.
var newCollection = list
.Where(m => (m.Month >= 3 && m.Month <=12 && m.Year == 2015) ||
(m.Month >=1 && m.Month <=2 && m.Year == 2016)
)
.GroupBy(m => new{m.Month, m.Year})
.Select(m => new {
year = m.Key.Year,
month = m.Key.Month,
AccNo21 = m.Sum(g => g.AccNo21),
AccNo22 = m.Sum(g => g.AccNo22)
});
You need to project it like this:-
.Select(a => new { Wages = a.Sum(b => b.Wages)})
and so on for AccNo22, AccNo21 etc.
Here is the complete query:-
var result= list.Where(x => (x.Month >= 3 && x.Month <=12 && x.Year == 2015) ||
(x.Month >=1 && x.Month <=2 && x.Year == 2016)
)
.GroupBy(x => new {x.Month, x.Year})
.Select(x => new {
Year = x.Key.Year,
Month = x.Key.Month,
AccNo21 = x.Sum(z => z.AccNo21),
AccNo22 = x.Sum(z => z.AccNo22)
});

linq-to-sql group by with count and custom object model

I'm looking to fill an object model with the count of a linq-to-sql query that groups by its key.
The object model looks somewhat like this:
public class MyCountModel()
{
int CountSomeByte1 { get; set; }
int CountSomeByte2 { get; set; }
int CountSomeByte3 { get; set; }
int CountSomeByte4 { get; set; }
int CountSomeByte5 { get; set; }
int CountSomeByte6 { get; set; }
}
This is what I have for the query:
var TheQuery = from x in MyDC.TheTable
where ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7
group x by x.SomeByte into TheCount
select new MyCountModel()
{
CountSomeByte1 = TheCount.Where(TheCount => TheCount.Key == 1)
.Select(TheCount).Count(),
CountSomeByte2 = TheCount.Where(TheCount => TheCount.Key == 2)
.Select(TheCount).Count(),
.....
CountSomeByte6 = TheCount.Where(TheCount => TheCount.Key == 6)
.Select(TheCount).Count(),
}.Single();
ListOfRecordIDs is list of longs that's passed in as a parameter. All the CountSomeByteN are underlined red. How do you do a count of grouped elements with the group's key mapped to an object model?
Thanks for your suggestions.
The select is taking each element of your group and projecting them to identical newly created MyCountModels, and you're only using one of them. Here's how I'd do it:
var dict = MyDC.TheTable
.Where(x => ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7)
.GroupBy(x => x.SomeByte)
.ToDictionary(grp => grp.Key, grp => grp.Count());
var result = new MyCountModel()
{
CountSomeByte1 = dict[1];
CountSomeByte2 = dict[2];
CountSomeByte3 = dict[3];
CountSomeByte4 = dict[4];
CountSomeByte5 = dict[5];
CountSomeByte6 = dict[6];
}
EDIT: Here's one way to do it in one statement. It uses an extension method called Into, which basically works as x.Into(f) == f(x). In this context, it can be viewed as like a Select that works on the whole enumerable rather than on its members. I find it handy for eliminating temporary variables in this sort of situation, and if I were to write this in one statement, it's probably how I'd do it:
public static U Into<T, U>(this T self, Func<T, U> func)
{
return func(self);
}
var result = MyDC.TheTable
.Where(x => ListOfRecordIDs.Contains(x.RecordID) && x.SomeByte < 7)
.GroupBy(x => x.SomeByte)
.ToDictionary(grp => grp.Key, grp => grp.Count())
.Into(dict => new MyCountModel()
{
CountSomeByte1 = dict[1];
CountSomeByte2 = dict[2];
CountSomeByte3 = dict[3];
CountSomeByte4 = dict[4];
CountSomeByte5 = dict[5];
CountSomeByte6 = dict[6];
});
Your range variable is not correct in the subqueries:
CountSomeByte6 = TheCount.Where(TheCount => TheCount.Key == 6)
.Select(TheCount).Count(),
In method notation you don't need the extra select:
CountSomeByte6 = TheCount.Where(theCount => theCount.Key == 6).Count(),
If you want to use it anyway:
CountSomeByte6 = TheCount.Where(theCount => theCount.Key == 6).Select(theCount => theCount).Count(),

Categories

Resources