I'm trying to get a dataset for an SSRS Line Chart. I am trying to show the number of records per week of the year for the last six months. In order for the line chart to be accurate, I have to show weeks that have zero records.
I want to provide the line chart with data that looks like this:
Year Week Count
2014 52 13
2015 1 0
2015 2 16
The following linq query gets everything I need minus the weeks that have zero records:
list = (from t in context.Cats.Where(
t => t.Name == "Fluffy" &&
(t.Recorded >= start && t.Recorded <= end)
).AsEnumerable()
group t by new { Year = t.Recorded.Year, WeekNumber = (t.Recorded - new DateTime(t.Recorded.Year, 1, 1)).Days / 7 } into ut
select new TrendRecord
{
Year = ut.Key.Year,
Week = ut.Key.WeekNumber,
Count = ut.Count()
}).OrderBy(t => t.Year).ThenBy(t => t.Week).ToList<TrendRecord>();
I've looked at this and this SO questions but neither seems to quite fit my predicament.
I'm leaning towards creating a list with all possible week numbers between the start and end dates and then left joining my data to it somehow so that weeks with 0 records show up.
Is this an acceptable approach to this problem(please show me how if so)? Is there a way to do this in one query without the need for a separate list to join to?
You will need a list of weeks and do some kind of LEFT OUTER JOIN using Linq.
Something like this should help you get started
var weeks = new[]
{
new {Year = 2014, Week = 52},
new {Year = 2015, Week = 1},
new {Year = 2015, Week = 2},
};
var listWithEmptyWeeksIncluded =
from w in weeks
from l in list.Where(x => x.Year == w.Year && x.Week == w.Week).DefaultIfEmpty()
select new TrendRecord
{
Year = w.Year,
Week = w.Week,
Count = l == null ? 0 : l.Count
};
Of course, the weeks should be generated on the basis of your variables start and end
Also, you should be using a standard week calculation, for instance the ISO week standard, so that weeks that span two years are handled correctly.
Related
I'm selecting an IEnumerable of DateTime by using a list of int which represent a year.
Each of the resulting DateTimes is given a default month and day of 1 so e.g.
List<int> years = new List<int>() { 2018, 2017, 2016 };
var dateTimes = years.Select(x => new DateTime(int.Parse(x), 1, 1));
Gives me 2018/01/01, 2017/01/01, 2016/01/01 (ignoring the time component)
I want to get the same results but for each month of each year too, so actually 36 results for three given years:
2018/01/01
2018/02/01
...
2018/11/01
2018/12/01
2017/01/01
...
2016/12/01
(this is using non-US culture datetime where months are the middle value)
I was wondering if C# had a really nice shortcut like the range notation
var dateTimes = years.Select(x => new DateTime(int.Parse(x), 1..12, 1));
But that certainly doesn't work.
Any syntactic-shortcut way to achieve this without just looping i = 1 to 12 style?
The best I came up with was:
var dateTimes = new List<DateTime>();
foreach(var y in years)
{
for (int i = 1; i <= 12; i++) dateTimes.Add(new DateTime(int.Parse(y), i, 1));
}
Which does exactly what I want but though there was a more succinct way to write.
var dateTimes = years.SelectMany(y => Enumerable.Range(1, 12).Select(m => new DateTime(y, m, 1))); // .ToList() if you want
Enumerable.Range() available from .NET 3.5
I previously asked this question here: List of Dates ordered in a certain way
I thought the proposed solution was fine until the year ticked over on my date list and encountered an issue.
My date list (in this string based format - this is how the data comes to me from the source API)
201711
201712
201801
201811
201812
201901
I want to present my data in a bar chart to show 3 months worth of year on year comparison in month order. This would mean I order the list as so
201711
201811
201712
201812
201801
201901
So I can then see year-on-year bars for November, December and Jan in that order.
I've tried the solution at the bottom of the question but it places the order like so (which is not what I want):
201801
201901
201711
201811
201712
201812
For clarity, next month it will need to move forward to be this date list:
the first month I want will always be 2 months before the current one
201712
201812
201801
201901
201802
201902
var rowsInOrder = dateList.OrderBy(n => DateTime.ParseExact(n, "yyyyMM", CultureInfo.CurrentCulture).Month).ThenBy(n=> DateTime.ParseExact(n, "yyyyMM", CultureInfo.CurrentCulture).Year);
You can use this Lookup approach that first determines the month-groups:
var monthLookup = dateList
.Select(s => new{String = s, Date = DateTime.ParseExact(s, "yyyyMM", CultureInfo.CurrentCulture)})
.OrderBy(x => x.Date) // not necessary in your sample data but i assume it's desired
.ToLookup(x=> x.Date.Month);
var rowsInOrder = monthLookup.SelectMany(x => x).Select(x => x.String);
I could manage to achieve your goal using GroupBy to group months,
var rowsInOrder = new List<string>();
foreach (var grouping in dates.GroupBy(s =>
DateTime.ParseExact(s, "yyyyMM", CultureInfo.CurrentCulture).Month))
{
rowsInOrder.AddRange(grouping.OrderBy(s => s));
};
You can also order months with same logic:
var rowsInOrder = new List<string>();
foreach (var grouping in dates
.OrderBy(s => DateTime.ParseExact(s, "yyyyMM", CultureInfo.CurrentCulture).Month).GroupBy(s =>
DateTime.ParseExact(s, "yyyyMM", CultureInfo.CurrentCulture).Month))
{
rowsInOrder.AddRange(grouping.OrderBy(s => s));
}
It seems to me that this is sufficient:
var rowsInOrder = dates.OrderBy(x => x).GroupBy(x => x.Substring(4)).SelectMany(x => x);
There's simply no need to muck around with parsing dates. It's a simple string sort and group this way.
So you are stuck with a sequence of objects, where every object has a Date property of type string in the format yyyyMM. and you want to extract some data from it.
Your date format is language independent. It doesn't matter whether your computer is a British one, or a Chinese one. The Date property will always be in format yyyyMM.
This makes it fairly easy to convert it into a DateTime format, which makes it easy to access the Year and Month.
const string dateTimeFormat = "yyyyMM";
CultureInfo provider = CultureInfo.InvariantCulture;
var dateList = ... // your original list of items with the string Date property
var itemsWithYearMonth = dateList.Select(item => new
{
DateTime = DateTime.ParseExact(item, dateTimeFormat, provider)
... // select other items you need for your bar chart
});
Now, given a StartYear/Month, a NrOfYears and a NrOfMonths you want to group dateTimeItems into groups of same month.
For example, starting at 2018-11, I want groups with four months, for three consecutive years (yeah, yeah, I know in your original request it was only 3 months, 2 years, but why limit yourself to this, let's make your code re-usable):
group 1: 2018-11, 2019-11, 2020-11, 2021-11
group 2: 2018-12, 2019-12, 2020-12, 2021-12
group 3: 2019-01, 2020-01, 2021-01, 2022-01
Bonus-points: we'll pass the year boundary!
So input:
var itemsWithYearMonth = ... // see above
int startYear = ...
int startMonth = ...
int nrOfMonths = ...
int nrOfYears = ...
We will make groups of items with same months. We don't want all Months of the year, we only want some months. If we want the 3 months starting at month 11, we need to keep the groups with Months 11, 12, 1.
var desiredMonths = Enumerable.Range(startMonth, nrOfMonths) // example: 11, 12, 13
.Select(monthNr => 1 + ((monthNr-1) % 12)); // 11, 12, 1
From your input, we don't want all months, we only want the year/month larger than the starting month.
DateTime startMonth = new DateTime(startYear, startMonth, 1);
The easiest way is to keep only the input source of items with date equal or larger than startMonth and take only the first NumberOfYears items of each group. This way you get the correct number of items if you pass the year boundary like I did in my example.
var result = itemsWithYearMonth
// keep only the items newer than startMonth
.Where(item => item.DateTime >= startMonth)
// group by same month:
.GroupBy(item => item.DateTime.Month,
(month, itemsWithThisMonth) => new
{
Month = month, // in my example: 11, 12, 1, ...
// in every group: take the first nrOfYears items:
Items = itemsWithThisMonth
// order by ascending year
.OrderBy(itemWithThisMonth => itemWithThisMonth.Year)
// no need to order by Month, all Months are equal in this group
.Take(nrOfYears)
.ToList(),
})
// keep only the desired months:
.Select(group => desiredMonth.Contains(group.Month));
So now you have groups:
group of month 11, with data of years 2018, 2019, 2020, 2021
group of month 12, with data of years 2018, 2019, 2020, 2021
group of month 01, with data of years 2019, 2020, 2021, 2022
I am having the amount field in income table in database, as well as the created date in same table. I need data like,
Week 1 => Sum(amount for week 1)
Week 2 => Sum(amount for week 2)
Week 3 => Sum(amount for week 3)
Week 4 => Sum(amount for week 4)
Week 5 => Sum(amount for week 5)
What should be my linq query. I am using entity framework.
Edited:
Say previous 4 week of current week + current week =5 weeks. here current week is the week of today's date. eg. today is 26'th Aug 2014 so current week is from 24'th Aug 2014 (Sunday) to 30'th Aug 2014 (Saturday).
You can use the methods in EntityFunctions to perform date and time arithmetic. So you should start by working out the start and end dates, then use TruncateTime if necessary to truncate your created date to a date (instead of date and time), and use DiffDays to work out "number of days since the start of the period". Then just divide by 7 to group...
DateTime today = DateTime.Today;
DateTime start = today.AddDays(-(int) today.DayOfWeek) // Sunday...
.AddDays(-28); // 4 weeks ago
DateTime end = start.AddDays(7 * 5);
var result = from entry in db.Entries
where entry.Created >= start && entry.Created < end
group entry.Amount by EntityFunctions.DiffDays(start,
EntityFunctions.TruncateTime(entry.Created)) / 7 into g
select new { Week = g.Key + 1, Sum = g.Sum() };
While I'd expect that to work, I haven't personally done any date/time work in EF myself. The general approach should be fine, it's just that you may need to tweak it. Also note that this won't give you any results for weeks that don't have any entries - it's probably easiest to do that outside EF.
EDIT: If the summing part isn't working, it's easy to do the summing locally instead:
var query = from entry in db.Entries
where entry.Created >= start && entry.Created < end
group entry.Amount by EntityFunctions.DiffDays(start,
EntityFunctions.TruncateTime(entry.Created)) / 7;
var result = query.AsEnumerable() // Execute the rest locally
.Select(g => new { Week = g.Key + 1, Sum = g.Sum() });
I'm building a query with LINQ to SQL in my C# project, but I have some problems with it...
What I want to do, is select the 4 lasts days that are like today (For example, a Friday), so if we're on Friday 28, I want to query for: Friday 21, 14, 7... The last four Fridays but NOT today.
This is easy, I've done that but here's the complex part, I want to not query the exceptions I set, for example End of month, which are from 28th to 1st day of each month, so let's say i want to query this (october, fridays):
Today is Friday 26, I want to query:
19, 12, 5 and September 28th (the fourth friday from now), but as I said, 28th is end of month, so i need to return September 21th which is the last friday and it is not end of month... I have the same issues with holidays, but I think if I can handle end of months, I can do with them...
i hope I've explained good for you to understand what I want... Here's my query, which is working but can't handle exceptions. (the field b.day is the Id for each days, 8 means end of month, and 7 holiday)
var values =
from b in dc.MyTable
where // This means end of month
b.day != 8
// This triggers to query last 4 days
&& b.date == Convert.ToDateTime(last.ToString("dd/MM/yyy")).AddDays(-28)
|| b.date == Convert.ToDateTime(last.ToString("dd/MM/yyy")).AddDays(-21)
|| b.date == Convert.ToDateTime(last.ToString("dd/MM/yyy")).AddDays(-14)
|| b.date == Convert.ToDateTime(last.ToString("dd/MM/yyy")).AddDays(-7)
orderby b.id descending
group b.valor by b.hora_id into hg
orderby hg.Key descending
select new
{
Key = hg.Key,
Max avg = System.Convert.ToInt32(hg.Average() + ((hg.Average() * intOkMas) / 100)),
Min avg = System.Convert.ToInt32(hg.Average() - ((hg.Average() * intOkMenos) / 100))
};
You should prepare the list of days you'd like retrieve before trying to query:
// Get the last four days excluding today on the same weekday
var days = Enumerable.Range(1, 4).Select(i => DateTime.Today.AddDays(i * -7));
Then remove any days you don't want:
// Remove those pesky end-of-month days
days = days.Where(d => d.Day < 28 && d.Day > 1);
When you're done preparing the list of days you want to retrieve, only then should you perform your query:
from b in dc.MyTable
where days.Contains(b.date) // Translated to SQL: date IN (...)
...
EDIT: As you mentioned in your comment, you want a total of four days even after any filtering you perform. So simply generate more days and take the first four:
var days = Enumerable.Range(1, int.MaxValue - 1)
.Select(i => DateTime.Today.AddDays(i * -7))
.Where(d => d.Day < 28 && d.Day > 1)
.Take(4);
Due to the way LINQ (and in general, enumerators) work, only four days plus any skipped days will be calculated.
Building on Allon Guralnek's answer, I'd modify it slightly:
First, build an infinite date generator:
public IEnumerable<DateTime> GetDaysLikeMe(DateTime currentDate)
{
DateTime temp = currentDate;
while(true)
{
temp = temp.AddDays(-7);
yield return temp;
}
}
Then you can use deferred execution to your advantage by limiting to only dates that meet your additional criteria:
GetDaysLikeMe(DateTime.Now).Where(dt => /* dt meets my criteria */).Take(4)
Then you can use this list that was generated to query in your LINQ to SQL like Allon Guralnek suggested above:
from b in dc.MyTable
where days.Contains(b.date) // Translated to SQL: date IN (...)
...
This has the benefit of you being able to specify additional predicates for what are acceptable dates and still get at least 4 dates back. Just be sure to put some bounds checking on the infinite date generator in case one of your predicates always returns false for whatever reason (which means the generator will never exit).
IE: while(temp > currentDate.AddYears(-1))
I would highly suggest writing your exception code (last friday of the month) after retrieving your rows, as this logic seems to be too complicated for a LINQ statement. Instead of retrieving the last 4 days, retrieve the last 5. Remove any that are the last Friday of each respective months. If you still have 5 rows, remove the last one.
Update
var values1 =
from b in dc.MyTable
where // This means end of month
b.day != 8
// This triggers to query last 4 days
&& b.date == Convert.ToDateTime(last.ToString("dd/MM/yyy")).AddDays(-28)
|| b.date == Convert.ToDateTime(last.ToString("dd/MM/yyy")).AddDays(-21)
|| b.date == Convert.ToDateTime(last.ToString("dd/MM/yyy")).AddDays(-14)
|| b.date == Convert.ToDateTime(last.ToString("dd/MM/yyy")).AddDays(-7)
orderby b.id descending
select b;
//Do stuff with values
var values2 = from b in values2
group b.valor by b.hora_id into hg
orderby hg.Key descending
select new
{
Key = hg.Key,
Max avg = System.Convert.ToInt32(hg.Average() + ((hg.Average() * intOkMas) / 100)),
Min avg = System.Convert.ToInt32(hg.Average() - ((hg.Average() * intOkMenos) / 100))
};
I need to compare the scores of a user between last week and two weeks ago.
i have a table like so
Scores
user score subject date
2 10 math 21/10/2012
2 5 science 23/10/2012
2 5 math 16/10/2012
2 9 science 15/12/2012
I need to produce a query that shows last week's score and whether it is an increase or decrease from 2 weeks ago's score
user score subject date increase/decrease
2 10 math 21/10/2012 +5
2 10 science 23/10/2012 -4
The date column doesn't need to be included in the query
I already have the code to get week range from last week to two weeks ago. I'm having trouble comparing the two dates however.
DateTime date = DateTime.Now;
DateTime startOneWeekAgo = date.AddDays(-7).Date.AddDays(-(int)date.DayOfWeek),
endOneWeekAgo = startOneWeekAgo.AddDays(7);
DateTime startTwoWeeksAgo = startOneWeekAgo.AddDays(-7),
endTwoWeeksAgo = endOneWeekAgo.AddDays(-7);
from s in Scores where s.scoredate >= startOneWeekAgo && s.scoredate <
endOneWeekAgo
this results in. This is what I have so far. Help would be appreciated.
user score subject
2 10 math
2 5 science
DateTime beginningOfWeek = DateTime.Now.BeginningOfWeek();
DateTime twoWeeksAgo = beginningOfWeek.AddDays(-14);
DateTime endOfLastWeek = beginningOfWeek.AddMilliseconds(-1);
var query = from s in scores
where s.Date >= twoWeeksAgo && s.Date <= endOfLastWeek
orderby s.Date
group s by new { s.User, s.Subject } into g
select new
{
User = g.Key.User,
Subject = g.Key.Subject,
Date = g.Last().Date,
Diff = g.Last().Score - g.First().Score
};
Thus you always selecting only two last scores (two weeks ago and one week ago), you will have only two records in each group.
If you will have only one week's results in database, then difference will be zero, because last and first entry in group will be the same.
Also you can use some DateTime extension for obtaining beginning of week:
public static class DateExtensions
{
public static DateTime BeginningOfWeek(this DateTime date,
DayOfWeek firstDayOfWeek = DayOfWeek.Monday)
{
if (date.DayOfWeek == firstDayOfWeek)
return date.Date;
DateTime result = date.AddDays(-1);
while (result.DayOfWeek != firstDayOfWeek)
result = result.AddDays(-1);
return result.Date;
}
}
And one more thing - try to avoid declaring several variables at once.