Linq query to count and group items by date - c#

I have a collection of Orders which are pulled from EF. Each Order has an order date:
public class Order {
public DateTime Date { get; set; }
public int Id { get; set; }
}
I want to be able to run a query to return the number of orders for each day in a certain date range. The query method should look something like:
public class ICollection<OrderDateSummary> GetOrderTotalsForDateRange(DateTime startDate, DateTime endDate) {
var orderDateSummary = Set.SelectMany(u => u.Orders) // ..... grouping/totalling here?!
return orderDateSummary;
}
For info, Set is actually part of a repository which returns a User aggregate root, so the type of Set is DbSet<User> The bit I am stuck on is grouping and totalling the Orders queryable from the SelectMany method.
The OrderDateSummary class looks like:
public OrderDateSummary {
DateTime Date { get; set; }
int Total { get; set; }
}
So, the output for a start date of 01/01/2016 and an end date of 03/01/2016 would look something like:
Date Total
===================
01/01/2016 10
02/01/2016 2
03/01/2016 0
04/01/2016 12

As I can see you need to generate all dates in range from start to end. Then calculate total number of orders on each date.
DateTime start = new DateTime(2016, 1, 1);
DateTime end = new DateTime(2016, 1, 4);
Enumerable
.Range(0, 1 + (end - start).Days)
.Select(x => start.AddDays(x))
.GroupJoin(Set.SelectMany(u => u.Orders),
dt => dt, o => o.Date.Date,
(dt, orders) => new OrderDateSummary { Date = dt, Total = orders.Count() })
.ToList();
Check out working example on Ideone.

var startDate = new DateTime (2016, 1, 1);
var endDate = new DateTime (2016, 1, 4);
Set.SelectMany(u => u.Orders).
Where (order => startDate <= order.Date && order.Date <= endDate) // If filter needed
GroupBy (order => order.Date, (date, values) =>
new OrderDateSummary () {
Date = date,
Total = values.Count ()
}).
OrderBy (summary => summary.Date).
ToList ();
Just you should mark your OrderDateSummary with class or struct and make those properties public or add constructor.
And you have a date 04/01/2016 in expected result, so, I guess, your end time is 4th and not 3th.

Try code below which is linq
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
namespace ConsoleApplication82
{
class Program
{
static void Main(string[] args)
{
List<OrderDateSummary> orderSummary = null;
DataTable dt = new DataTable();
dt.Columns.Add("id", typeof(int));
dt.Columns.Add("date", typeof(DateTime));
dt.Columns.Add("amount", typeof(decimal));
dt.Rows.Add(new object[] { 1, DateTime.Parse("1/1/16"), 1.00 });
dt.Rows.Add(new object[] { 2, DateTime.Parse("1/1/16"), 2.00 });
dt.Rows.Add(new object[] { 3, DateTime.Parse("1/2/16"), 3.00 });
dt.Rows.Add(new object[] { 4, DateTime.Parse("1/2/16"), 4.00 });
dt.Rows.Add(new object[] { 5, DateTime.Parse("1/2/16"), 5.00 });
dt.Rows.Add(new object[] { 6, DateTime.Parse("1/3/16"), 6.00 });
dt.Rows.Add(new object[] { 7, DateTime.Parse("1/3/16"), 7.00 });
orderSummary = dt.AsEnumerable()
.GroupBy(x => x.Field<DateTime>("date"))
.Select(x => new OrderDateSummary() { Date = x.Key, Total = x.Count() })
.ToList();
}
}
public class OrderDateSummary {
public DateTime Date { get; set; }
public int Total { get; set; }
}
}

how about
List<OrderDateSummary> Result = OrderList
.Where(x => x.Date >= startDate && x.Date <= endDate)
.GroupBy(x => x.Date)
.Select(z => new OrderDateSummary(){
Date = z.Key,
Total = z.Count()
}).OrderBy(d=> d.Date).ToList();

Related

Firebird group by period

I am pulling some historical data from Firebird database as below:
Product_ID Date Price
1 2001-01-01 10
1 2001-02-01 10
1 2001-03-01 15
1 2001-04-01 10
1 2001-05-01 20
1 2001-06-01 20
What I am trying to do is to extract the first for occurrence every price change.
Example of expected data set:
Product_ID Date Price
1 2001-01-01 10
1 2001-03-01 15
1 2001-04-01 10
1 2001-05-01 20
I know that on MSSQL I could leverage LAG for that. Is it possible to do that with Firebird?
You can try this, but be aware I didn't tested it:
CREATE PROCEDURE SP_Test
RETURNS (
Product_ID INTEGER,
Date DATE,
Price INTEGER
)
AS
DECLARE VARIABLE Last_Product_ID INTEGER;
DECLARE VARIABLE Last_Date DATE;
DECLARE VARIABLE Last_Price INTEGER;
BEGIN
FOR SELECT Product_ID, Date, Price
FROM xxxx
ORDER BY Product_ID, Date
INTO Product_ID, Date, Price
DO BEGIN
IF ((:Last_Product_ID IS NULL) OR
(:Last_Date IS NULL) OR
(:Last_Price IS NULL) OR
(:Product_ID <> :Last_Product_ID) OR
(:Price <> :Last_Price)) THEN
SUSPEND;
Last_Product_ID = :Product_ID;
Last_Date = :Date;
Last_Price = :Price;
END;
END;
in MoreLinq there is a Lag extension method but it is supported only in Linq to Objects...
What you can do, if you are looking for a C# linq answer for that you can:
Basically order your data the correct way and then add a row index for while price (and product_id) is still the same. Then group by it and select the min date.
int groupingIndex = 0;
int previousPrice = 0;
var response = data
.OrderBy(item => item.Product_ID)
.ThenBy(item => item.Date)
.Select(item =>
{
if (item.Price != previousPrice)
{
previousPrice = item.Price;
groupingIndex++;
}
return new { Index = groupingIndex, Item = item };
})
.GroupBy(item => new { item.Index, item.Item.Product_ID, item.Item.Price } )
.Select(group => new Record
{
Product_ID = group.Key.Product_ID,
Price = group.Key.Price,
Date = group.Min(item => item.Item.Date)
}).ToList();
And if you don't mind doing the operation in the C# and not the DB (and using a beta version of the MoreLinq) then:
int index = 0;
var result2 = data
.OrderBy(item => item.Product_ID)
.ThenBy(item => item.Date)
.Lag(1, (current, previous) => new { Index = (current.Price == previous?.Price ? index : ++index), Item = current })
.GroupBy(item => new { item.Index, item.Item.Product_ID, item.Item.Price })
.Select(group => new Record { Product_ID = group.Key.Product_ID, Price = group.Key.Price, Date = group.Min(item => item.Item.Date) })
.ToList();
This is a little complicated but it works
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
namespace ConsoleApplication6
{
class Program
{
static void Main(string[] args)
{
DataTable dt = new DataTable();
dt.Columns.Add("Product_ID", typeof(int));
dt.Columns.Add("Date", typeof(DateTime));
dt.Columns.Add("Price", typeof(int));
dt.Rows.Add(new object[] {1, DateTime.Parse("2001-01-01"), 10});
dt.Rows.Add(new object[] {1, DateTime.Parse("2001-02-01"), 10});
dt.Rows.Add(new object[] {1, DateTime.Parse("2001-03-01"), 15});
dt.Rows.Add(new object[] {1, DateTime.Parse("2001-04-01"), 10});
dt.Rows.Add(new object[] {1, DateTime.Parse("2001-05-01"), 20});
dt.Rows.Add(new object[] {1, DateTime.Parse("2001-06-01"), 20});
dt.Rows.Add(new object[] { 2, DateTime.Parse("2001-01-01"), 10 });
dt.Rows.Add(new object[] { 2, DateTime.Parse("2001-02-01"), 10 });
dt.Rows.Add(new object[] { 2, DateTime.Parse("2001-03-01"), 15 });
dt.Rows.Add(new object[] { 2, DateTime.Parse("2001-04-01"), 10 });
dt.Rows.Add(new object[] { 2, DateTime.Parse("2001-05-01"), 20 });
dt.Rows.Add(new object[] { 2, DateTime.Parse("2001-06-01"), 20 });
dt = dt.AsEnumerable().OrderBy(x => x.Field<DateTime>("Date")).CopyToDataTable();
List<DataRow> results = dt.AsEnumerable()
.GroupBy(g => g.Field<int>("Product_ID"))
.Select(g1 => g1.Select((x, i) => new { row = x, dup = (i == 0) || ((i > 0) && (g1.Skip(i - 1).FirstOrDefault().Field<int>("Price") != g1.Skip(i).FirstOrDefault().Field<int>("Price"))) ? false : true })
.Where(y => y.dup == false).Select(z => z.row)).SelectMany(m => m).ToList();
}
}
}

Return first x items in a group of groups

I'm querying a datatable and I seem stuck on selecting a group of groups.
This code
var grouping = table.AsEnumerable()
.Where(x => curveids.Contains(x.Field<short>("CurveID")) && x.Field<DateTime>("Timestamp").Hour >= hour && x.Field<DateTime>("Timestamp").Hour < (hour + 1))
.GroupBy(x => x.Field<DateTime>("Timestamp")).Where(x => x.Select(y => y["CurveID"]).Count() == curveids.Count);
Groups by timestamp and returns a group of x curves, where x = curveid.Count(). It contains 5000ish groups.
However for each day there can be more than one timestamp.
int nrdays = grouping.GroupBy(z => z.Key.Date).Count();
tells me there are 255 distinct days.
I would now like to group this again, but not by time stamp but by calendar day and then take the first (as in earliest) group for each day. I tried this:
var grouping2 = grouping.GroupBy(z => z.Key.Date).OrderBy(a => a.Key).Take(curveids.Count);
but this only returns 4 groups and I dont get why?
It should return 255 groups with each of them containing the same timestamp and x curveids, so x*255 record sets.
The datatable has 3 columns, Timestamp (DateTime), CurveID(short), Price(double).
UPDATE
As requested by Mr Skeet a full example:
public class listprx
{
public DateTime timestamp;
public int curveID;
public double prx;
}
static void Main(string[] args)
{
var data = new List<listprx>();
// populating data
for (int i = 0; i < 50000; i++)
{
Random rand = new Random(i);
var tempdt = new DateTime(2016, rand.Next(1, 12), rand.Next(1, 29), rand.Next(1, 23), rand.Next(1, 59), 0);
if(i % 3 == 0)
{
data.Add(new listprx { timestamp = tempdt, curveID = 1, prx = rand.Next(1,50)});
data.Add(new listprx { timestamp = tempdt, curveID = 2, prx = rand.Next(1, 50) });
}
else if (i % 5 == 0)
{
data.Add(new listprx { timestamp = tempdt, curveID = 1, prx = rand.Next(1, 50) });
}
else
{
data.Add(new listprx { timestamp = tempdt, curveID = 1, prx = rand.Next(1, 50) });
data.Add(new listprx { timestamp = tempdt, curveID = 2, prx = rand.Next(1, 50) });
data.Add(new listprx { timestamp = tempdt, curveID = 3, prx = rand.Next(1, 50) });
}
}
// setting hour criteria
int hour = 16;
int nrcurves = 3;
// grouping by timestamp and only take those where all curves are there, (as close to the desired time as possible
var grouping = data.Where(x => x.timestamp.Hour >= hour && x.timestamp.Hour < (hour + 1))
.GroupBy(x => x.timestamp).Where(x => x.Select(y => y.curveID).Count() == nrcurves);
// Grouping by day and take only the time stamp that is closest to the hour
// this fails
var grouping2 = grouping.GroupBy(z => z.Key.Date).OrderBy(a => a.Key).Take(nrcurves);
Console.WriteLine("Nr of timestamps with all curves {0}, nr of days {1}, nr of groups in second group {2}, expected same as nr days"
, grouping.Count(), grouping.GroupBy(z => z.Key.Date).Count(), grouping2.Count());
Console.ReadLine();
}
UPDATE 2
I have removed the random element and simplified further:
public class listprx
{
public DateTime timestamp;
public int curveID;
}
static void Main(string[] args)
{
var data = new List<listprx>();
// populating data
var tempdt = new DateTime(2016, 4, 6, 16, 1, 0);
for (int i = 0; i < 4; i++)
{
if (i == 2)
{
tempdt = tempdt.AddDays(1);
}
if(i % 2 == 0 )
{
data.Add(new listprx { timestamp = tempdt, curveID = 1});
}
else
{
data.Add(new listprx { timestamp = tempdt, curveID = 1});
data.Add(new listprx { timestamp = tempdt, curveID = 2});
}
tempdt = tempdt.AddMinutes(i+1);
}
// setting hour criteria
int hour = 16;
int nrcurves = 2;
//grouping by timestamp and only take those where all curves are there, (as close to the desired time as possible
var grouping = data.Where(x => x.timestamp.Hour >= hour && x.timestamp.Hour < (hour + 1))
.GroupBy(x => x.timestamp).Where(x => x.Select(y => y.curveID).Count() == nrcurves);
//Grouping by day and take only the time stamp that is closest to the hour
//this fails
var grouping2 = grouping.GroupBy(z => z.Key.Date).OrderBy(a => a.Key).Take(nrcurves);
Console.WriteLine("Nr of timestamps with all curves {0}, nr of days {1}, nr of groups in second group {2}, expected same as nr days"
, grouping.Count(), grouping.GroupBy(z => z.Key.Date).Count(), grouping2.Count());
Console.ReadLine();
}
The expected end result is:
Timestamp CurveID
------------------------
6/4/16 16:02 1
6/4/16 16:02 2
7/4/16 16:06 1
7/4/16 16:06 2
Edited answer working on your example.
Ok, I went trought your example and fixed some bugs and my answer. Let's clear code a bit and comment what went wrong where.
Our models will be
public class Curve
{
public int CurveID { get; set; }
public DateTime Timestamp { get; set; }
}
public class CurveGroup
{
public DateTime Timestamp { get; set; }
public IEnumerable<Curve> Curves { get; set; }
}
next is function to generate test data:
public static List<Curve> GetData()
{
var data = new List<Curve>();
var startTime = new DateTime(2016, 4, 6, 16, 1, 0);
for (int i = 0; i < 4; i++)
{
if (i == 2)
{
//startTime.AddDays(1); - this line does nothing, DateTime is an immutable struct so all function changing its value returns a new copy
startTime = startTime.AddDays(1);
}
if (i % 2 == 0)
{
data.Add(CreateNewCurve(startTime, 1));
}
else
{
data.Add(CreateNewCurve(startTime, 1));
data.Add(CreateNewCurve(startTime, 2));
}
//startTime.AddMinutes(i + 1); same issue as above
startTime = startTime.AddMinutes(i + 1);
}
return data;
}
public static Curve CreateNewCurve(DateTime time, int curveID)
{
return new Curve()
{
Timestamp = time,
CurveID = curveID
};
}
and here goes main function
static void Main(string[] args)
{
var data = GetData();
int hour = 16;
int totalCurveCount = 2;
var grouping = data
.Where(x => x.Timestamp.Hour >= hour && x.Timestamp.Hour < (hour + 1))
.GroupBy(x => x.Timestamp)
.Where(x => x.Count() == totalCurveCount); //there is no need to select curveId like in your code: Where(x => x.Select(y => y.curveID).Count() == nrcurves);
var grouping2 = grouping
.GroupBy(x => x.Key.Date)
.Select(x =>
new CurveGroup
{
Timestamp = x.Key,
Curves = x.OrderBy(c => c.Key).Take(totalCurveCount).SelectMany(c => c)
}
);
foreach (var g in grouping2)
{
foreach (var c in g.Curves)
{
Console.WriteLine(c.Timestamp);
Console.WriteLine(c.CurveID);
}
}
}
this returns expected results.
Your code failed because your second grouping is not taking (Take(nrcurves)) values in groups but groups themselves. So instead of returning 255 groups with 2 values in each you return 2 groups with all values in them.
Hope this fixes your issue.

Aggregate hours between two dates with IQueryable from DB

I have table with these columns:
id int, name string, startDate DateTime, endDate DateTime
I want to get from DB SUM of HOURS between these dates for all records.
I use IQueryable, but I don't know how correctly form the query..
public int GetSumOfHours(int? assignedProjectId)
{
var query = GetAll(); //GetAll() returns IQueryable<T>,
if (assignedProjectId.HasValue)
{
query = query.Where(solution => solution.AssignedProject.Id == assignedProjectId.Value);
}
// Get sum of hours ----> query = query...??
}
Thanks for HELP !
Try something like below. Get seconds and then sum the seconds.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
DataTable query = new DataTable();
query.Columns.Add("id", typeof(int));
query.Columns.Add("name", typeof(string));
query.Columns.Add("startDate", typeof(DateTime));
query.Columns.Add("endDate", typeof(DateTime));
query.Rows.Add(new object[] { 1, "John", DateTime.Parse("1/1/1 0:0:12"), DateTime.Parse("1/1/1 0:1:12") });
query.Rows.Add(new object[] { 1, "John", DateTime.Parse("1/1/1 0:3:12"), DateTime.Parse("1/1/1 0:5:12") });
query.Rows.Add(new object[] { 2, "Bob", DateTime.Parse("1/1/1 0:0:12"), DateTime.Parse("1/1/1 0:1:12") });
query.Rows.Add(new object[] { 2, "Bob", DateTime.Parse("1/1/1 0:0:12"), DateTime.Parse("1/1/1 0:1:12") });
var totalSeconds = query.AsEnumerable()
.Where(x => x.Field<int>("id") == 1)
.Select(x => (x.Field<DateTime>("endDate") - x.Field<DateTime>("startDate")).TotalSeconds).Sum();
}
}
}
​
You can use some math and the Sum() method:
public class hours
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
...
List<hours> allHrs = new List<hours>{
new hours{Start = DateTime.Now.AddHours(-3.2), End = DateTime.Now.AddHours(-2)},
new hours{Start = DateTime.Now.AddHours(-3.9), End = DateTime.Now.AddHours(-2.03)},
new hours{Start = DateTime.Now.AddHours(-3.8), End = DateTime.Now.AddHours(-2.9)}
};
//Project a new collection with the math done for number of minutes in each row
var mins = from h in allHrs
select new {nbrMinutes = (h.End - h.Start).Minutes};
//Sum all rows, divide by 60 to get hours
Double total = mins.Sum (s => s.nbrMinutes / 60.0 );
Console.WriteLine(total);
You could modify CrowCoder's example to sum the hours directly using TimeSpan and methods Subtract and TotalHours:
{
DateTime d = DateTime.Today;
List<hours> exampleHours = new List<hours>{
new hours{Start = d.AddHours(-3.2), End = d.AddHours(-2)},
new hours{Start = d.AddHours(-3.9), End = d.AddHours(-2.03)},
new hours{Start = d.AddHours(-3.8), End = d.AddHours(-2.9)}
};
double totalHours =
(from h in exampleHours
select new {allHours = (h.End.Subtract(h.Start).TotalHours)})
.Sum(t => t.allHours);
Console.WriteLine(totalHours.ToString());
}

Linq produce a list of missing records for a given date range

Say I have a list of the following class:
public class Holding
{
public string HoldingId{ get; set; }
public DateTime date { get; set; }
}
There needs to be a holding for each day in a given date range. I need to be able to produce a list of holdings that are missing for the range.
So say I have the following data that I need to check over the range 1 June 2010 - 5 June 2010:
HoldingId Date
1 01-06-2010
1 02-06-2010
1 04-06-2010
2 02-06-2010
2 03-06-2010
2 05-06-2010
3 03-06-2010
For this set of data the missing holdings would be:
HoldingId Date
1 03-06-2010
1 05-06-2010
2 01-06-2010
2 04-06-2010
3 01-06-2010
3 02-06-2010
3 04-06-2010
3 05-06-2010
I have produced the list range of dates using the answer to the following question:
Find missing dates for a given range.
I can't quite get my head around how to go forward from here...I assume I'll need to group by HoldingId to produce an array of dates and then do range.Except(holdings.dates) or something to that effect.
Does anyone have a nice solution to this problem using Linq?
you're quite right in howit should be done; here is what I got;
List<Holding> holdings = new List<Holding>();
holdings.Add(new Holding(){ date=Convert.ToDateTime("01-06-2010"), HoldingId = "1" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("02-06-2010"), HoldingId = "1" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("04-06-2010"), HoldingId = "1" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("02-06-2010"), HoldingId = "2" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("03-06-2010"), HoldingId = "2" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("05-06-2010"), HoldingId = "2" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("03-06-2010"), HoldingId = "3" });
List<DateTime> dateRange = new List<DateTime>();
dateRange.Add(Convert.ToDateTime("01-06-2010"));
dateRange.Add(Convert.ToDateTime("02-06-2010"));
dateRange.Add(Convert.ToDateTime("03-06-2010"));
dateRange.Add(Convert.ToDateTime("04-06-2010"));
dateRange.Add(Convert.ToDateTime("05-06-2010"));
Dictionary<string, List<DateTime>> missingHoldings = new Dictionary<string, List<DateTime>>();
foreach(var holdGrp in holdings.GroupBy (h => h.HoldingId))
{
var missingDates = dateRange.Except(holdGrp.Select(h => h.date)).ToList();
missingHoldings.Add(holdGrp.Key, missingDates);
}
An alternative approach:
public static List<Holding> MissingHoldings(List<Holding> existingHoldings, DateTime startDate, DateTime endDate)
{
var missingHoldings = new List<Holding>();
var holdingIds = existingHoldings.Select(h => h.HoldingId).Distinct().ToList();
var dates = new List<DateTime>();
for (var current = startDate.Date; current <= endDate.Date; current = current.AddDays(1))
{
dates.Add(current);
}
foreach (var holdingId in holdingIds)
{
missingHoldings
.AddRange(
dates.Where(date => !existingHoldings.Any(h => h.HoldingId == holdingId && h.date == date))
.Select(date => new Holding {HoldingId = holdingId, date = date}));
}
return missingHoldings;
}
A pure Linq Query inspired by saj's answer :
var missingHoldingsList =
from h in holdings.GroupBy( h => h.HoldingId )
from d in dateRange.Except( h.Select(x => x.date) )
orderby h.Key, d
select new Holding { date = d , HoldingId = h.Key };
and a loop-less version of saj's answer:
var missingHoldingsDict = (
from h in holdings.GroupBy(h => h.HoldingId)
select new
{
key = h.Key,
holdings =
from d in dateRange.Except(h.Select(x => x.date))
select new Holding { date = d, HoldingId = h.Key }
}
).ToDictionary(
h => h.key,
h => h.holdings.ToList()
);

How do I make this NHibernate QueryOver query return rows for empty groups

The following NHibernate QueryOver query is counting the number of applications for each month, within a given date range.
However, I don't get any results for months that don't have any applications in them but I want to actually have Count = 0 returned for those months.
So how would I change the query to return a row as well for months that don't have any applications in them?
DateTimeOffset endDate = DateTimeOffset.Now;
DateTimeOffset startDate = endDate.AddMonths(-12);
var result = Session.QueryOver<Application>()
.WhereRestrictionOn(c => c.SubmissionDate).IsBetween(startDate).And(endDate)
.SelectList(list => list
.Select(Projections.SqlGroupProjection(
"YEAR(SubmissionDate) As [Year]",
"YEAR(SubmissionDate)",
new[] { "YEAR" },
new IType[] { NHibernateUtil.Int32 }))
.Select(Projections.SqlGroupProjection(
"MONTH(SubmissionDate) As [Month]",
"MONTH(SubmissionDate)",
new[] { "MONTH" },
new IType[] { NHibernateUtil.Int32 }))
.SelectCount(x => x.Id))
.OrderBy(Projections.SqlFunction(
"YEAR",
NHibernateUtil.Int32,
Projections.Property<Application>(item => item.SubmissionDate))).Asc
.ThenBy(Projections.SqlFunction(
"MONTH",
NHibernateUtil.Int32,
Projections.Property<Application>(item => item.SubmissionDate))).Asc
.List<object[]>()
.Select(n => new
{
Year = n[0],
Month = n[1],
Count = (int)n[2]
}));
Update: taking your idea with DateTime.AddMonths() it gets even shorter
DateTime lastMonth = startdate;
var unionresults = result.SelectMany(r =>
{
var actualDate = new DateTime(r.Year, r.Month, 1);
var results = Enumerable.Repeat(1, Months)
.Select(i => lastMonth.AddMonths(i))
.TakeWhile(date => date < actualDate)
.Select(date => new { Year = date.Year, Month = date.Month, Count = 0 })
.Concat(new[] { r });
lastMonth = actualDate;
return results;
});
Original:
i think you have to add that data after the query. here an example using linq to fill in missing months
var result = <query>;
int lastMonth = 1;
var unionresults = result.SelectMany(r =>
{
var results = new[] { r }.AsEnumerable();
if (lastMonth > r.Month)
{
results = Enumerable.Range(lastMonth, 12 - lastMonth).Select(month => new { Year = r.Year, Month = month, Count = 0 })
.Concat(Enumerable.Range(1, r.Month).Select(month => new { Year = r.Year, Month = month, Count = 0 }))
.Concat(results);
}
else if (lastMonth < r.Month)
{
results = Enumerable.Range(lastMonth, r.Month - lastMonth)
.Select(month => new { Year = r.Year, Month = month, Count = 0 })
.Concat(results);
}
lastMonth = r.Month + 1;
if (lastMonth > 12)
{
lastMonth = 1;
}
return results;
});
It cannot be done with a few simple changes. The SQL query that is generated by your QueryOver() cannot count what does not exist in the first place.
You could probably do it with a UNION or a JOIN using a virtual/temporary table (depending on the DBMS) but that would make the query overly complicated.
I suggest adding a loop after your query that iterates through the list, copies the elements to a new list and adds any non-existing months to that new list. Something like this:
class YearMonthCount
{
public int Year { get; set; }
public int Month { get; set; }
public int Count { get; set; }
}
// Start and End dates
DateTime startDate = new DateTime(2011, 9, 1);
DateTime endDate = new DateTime(2012, 6, 1);
// this would be a sample of the QueryOver() result
List<YearMonthCount> result = new List<YearMonthCount>();
result.Add(new YearMonthCount { Year = 2011, Month = 10, Count = 2 });
result.Add(new YearMonthCount { Year = 2011, Month = 11, Count = 3 });
result.Add(new YearMonthCount { Year = 2012, Month = 1, Count = 4 });
result.Add(new YearMonthCount { Year = 2012, Month = 2, Count = 1 });
result.Add(new YearMonthCount { Year = 2012, Month = 4, Count = 1 });
result.Add(new YearMonthCount { Year = 2012, Month = 5, Count = 1 });
int i = 0;
List<YearMonthCount> result2 = new List<YearMonthCount>();
// iterate through result list, add any missing entry
while (startDate <= endDate)
{
bool addNewEntry = true;
// check to avoid OutOfBoundsException
if (i < result.Count)
{
DateTime listDate = new DateTime(result[i].Year, result[i].Month, 1);
if (startDate == listDate)
{
// entry is in the QueryOver result -> add this
result2.Add(result[i]);
i++;
addNewEntry = false;
}
}
if (addNewEntry)
{
// entry is not in the QueryOver result -> add a new entry
result2.Add(new YearMonthCount {
Year = startDate.Year, Month = startDate.Month, Count = 0 });
}
startDate = startDate.AddMonths(1);
}
This could probably be done more elegantly but it gets the job done.
Thanks to all the answers, this is how I ended up doing it:
DateTime endDate = DateTime.Now;
DateTime startDate = endDate.AddMonths(-Months);
var result = Session.QueryOver<Application>()
.WhereRestrictionOn(c => c.SubmissionDate).IsBetween(startDate).And(endDate)
.SelectList(list => list
.Select(Projections.SqlGroupProjection(
"YEAR(SubmissionDate) As [Year]",
"YEAR(SubmissionDate)",
new[] { "YEAR" },
new IType[] { NHibernateUtil.Int32 }))
.Select(Projections.SqlGroupProjection(
"MONTH(SubmissionDate) As [Month]",
"MONTH(SubmissionDate)",
new[] { "MONTH" },
new IType[] { NHibernateUtil.Int32 }))
.SelectCount(x => x.Id))
.List<object[]>()
.Select(n => new
{
Year = (int)n[0],
Month = (int)n[1],
Count = (int)n[2]
}).ToList();
var finalResult = result
.Union(
Enumerable.Range(0, Months - 1).Select(n => new
{
Year = startDate.AddMonths(n).Year,
Month = startDate.AddMonths(n).Month,
Count = 0
})
.Where(n => !result.Any(r => r.Year == n.Year && r.Month == n.Month)))
.OrderBy(n => n.Year).ThenBy(n => n.Month);

Categories