How to get gap in date ranges from a period of time - c#

I have an initial and a final date range = 1/1/2015 - 1/30/2015
I have these date ranges that represent dates of unavailability.
1/5/2015 - 1/10/2015
1/15/2015 - 1/20/2015
1/22/2015 - 1/28/2015
I want this output, mainly the dates of availability from the main range:
A: 1/1/2015 - 1/4/2015
B: 1/11/2015 - 1/14/2015
C: 1/21/2015 - 1/21/2015
D: 1/29/2015 - 1/30/2015
I tried to generate a sequential date range like this in order to get the exception dates with Except() but I think I'm complicating the thing.
//dtStartDate = 1/1/2015
//dtEndDate = 1/30/2015
var days = (int)(dtEndDate - dtStartDate).TotalDays + 1;
var completeSeq = Enumerable.Range(0, days).Select(x => dtStartDate.AddDays(x)).ToArray();
How can I get the gap of date ranges from period of time.
I other words how can I get the A, B, C and D from this picture
http://www.tiikoni.com/tis/view/?id=ebe851c
If these dates overlap, they must not be considered only where is a gap.
----------UPDATE-----------
I think if I do this:
var range = Enumerable.Range(0, (int)(1/10/2015 - 1/5/2015).TotalDays + 1).Select(i => 1/5/2015.AddDays(i));
var missing = completeSeq.Except(range).ToArray();
for each date range I will have the exclusion of each date range given but still cannot get the gap!

I saw your question in my morning today and really liked it, but was busy the whole day. So, got a chance to play with your question and believe me I enjoyed it. Here is my code:-
DateTime startDate = new DateTime(2015, 1, 1);
DateTime endDate = new DateTime(2015, 1, 30);
int totalDays = (int)(endDate - startDate).TotalDays + 1;
availability.Add(new Availability { StartDate = endDate, EndDate = endDate });
var result = from x in Enumerable.Range(0, totalDays)
let d = startDate.AddDays(x)
from a in availability.Select((v, i) => new { Value = v, Index = i })
where (a.Index == availability.Count - 1 ?
d <= a.Value.StartDate : d < a.Value.StartDate)
&& (a.Index != 0 ? d > availability[a.Index - 1].EndDate : true)
group new { d, a } by a.Value.StartDate into g
select new
{
AvailableDates = String.Format("{0} - {1}",g.Min(x => x.d),
g.Max(x => x.d))
};
This, definitely need explanation so here it is:-
Step 1: Create a range of dates from Jan 01 till Jan 30 using Enumerable.Range
Step 2: Since after the second unavailable date range, we need to limit the dates selected from last endate till current object startdate, I have calculated index so that we can get access to the last enddate.
Step 3: Once we get the index, all we need to do is filter the dates except for first date range since we didn't have last object in this case.
Step 4: For the last item since we don't have the max range I am adding the endDate to our unavailable list (hope this makes sense).
Here is the Working Fiddle, if you get confused just remove group by and other filters and debug and see the resulting output it will look fairly easy :)

using System;
using System.Collections.Generic;
using System.Linq;
public static class Program {
public static void Main() {
Tuple<DateTime,DateTime> range=Tuple.Create(new DateTime(2015,1,1),new DateTime(2015,1,30));
Tuple<DateTime,DateTime>[] exclude=new[] {
Tuple.Create(new DateTime(2015,1,5),new DateTime(2015,1,10)),
Tuple.Create(new DateTime(2015,1,15),new DateTime(2015,1,20)),
Tuple.Create(new DateTime(2015,1,22),new DateTime(2015,1,28))
};
foreach(Tuple<DateTime,DateTime> r in ExcludeIntervals(range,exclude)) {
Console.WriteLine("{0} - {1}",r.Item1,r.Item2);
}
}
public static IEnumerable<Tuple<DateTime,DateTime>> ExcludeIntervals(Tuple<DateTime,DateTime> range,IEnumerable<Tuple<DateTime,DateTime>> exclude) {
IEnumerable<Tuple<DateTime,bool>> dates=
new[] { Tuple.Create(range.Item1.AddDays(-1),true),Tuple.Create(range.Item2.AddDays(1),false) }.
Concat(exclude.SelectMany(r => new[] { Tuple.Create(r.Item1,false),Tuple.Create(r.Item2,true) })).
OrderBy(d => d.Item1).ThenBy(d => d.Item2); //Get ordered list of time points where availability can change.
DateTime firstFreeDate=default(DateTime);
int count=1; //Count of unavailability intervals what is currently active. Start from 1 to threat as unavailable before range starts.
foreach(Tuple<DateTime,bool> date in dates) {
if(date.Item2) { //false - start of unavailability interval. true - end of unavailability interval.
if(--count==0) { //Become available.
firstFreeDate=date.Item1.AddDays(1);
}
} else {
if(++count==1) { //Become unavailable.
DateTime lastFreeDate=date.Item1.AddDays(-1);
if(lastFreeDate>=firstFreeDate) { //If next unavailability starts right after previous ended, then no gap.
yield return Tuple.Create(firstFreeDate,lastFreeDate);
}
}
}
}
}
}
ideone.com

Got a little oopy...
public class DateRange
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public bool HasStart
{
get { return Start != DateTime.MinValue; }
}
public bool IsInRange(DateTime date)
{
return (date >= this.Start && date <= this.End);
}
public List<DateRange> GetAvailableDates(DateRange excludedRange)
{
return GetAvailableDates(new List<DateRange>(){excludedRange});
}
public List<DateRange> GetAvailableDates(List<DateRange> excludedRanges)
{
if (excludedRanges == null)
{
return new List<DateRange>() { this };
}
var list = new List<DateRange>();
var aRange = new DateRange();
var date = this.Start;
while (date <= this.End)
{
bool isInARange = excludedRanges.Any(er => er.HasStart && er.IsInRange(date));
if (!isInARange)
{
if (!aRange.HasStart)
{
aRange.Start = date;
}
aRange.End = date;
}
else
{
if (aRange.HasStart)
{
list.Add(aRange);
aRange = new DateRange();
}
}
date = date.AddDays(1);
}
if (aRange.HasStart)
{
list.Add(aRange);
}
return list;
}
}

Related

Filter or Check Date in A Date Range

It's totally a simple or basic requirement. I am trying to get a date from a list of date using C#. So what I've done, made a function and iterated that with a for loop. I've tried to make the list function into two ranges and passed the value from the DatePicker control as follows:
private void btnClick_Click(object sender, EventArgs e)
{
DateTime theFromDate = dateTimePicker1.Value;
DateTime theToDate = dateTimePicker2.Value;
List<DateRange> lstRange = GetDateRange();
/**Trying To Get The Date From The Range - Starts**/
var dates = new List<DateTime>();
for (var dt = theFromDate; dt <= theToDate; dt = dt.AddDays(1))
{
dates.Add(dt);
//MessageBox.Show(dt.Date.ToString());
}
List<DateRange> lst = GetDateRange();
foreach(var item in lst)
{
if(theFromDate <= item.EndtDate.Date)
{
MessageBox.Show(theFromDate.ToString("dd-MMM-yyyy") + " in the date range!");
}
else
{
MessageBox.Show(theFromDate.ToString("dd-MMM-yyyy") + " not in the date range!");
}
}
/**Trying To Get The Date From The Range - Ends**/
}
public class DateRange
{
public DateTime date { set; get; }
public DateTime EndtDate { set; get; }
}
/**List of Dates Here - Starts**/
public List<DateRange> GetDateRange()
{
List<DateRange> lstDate = new List<DateRange>();
DateRange aDateRange = new DateRange();
aDateRange.StartDate = Convert.ToDateTime("10-Aug-2018");
aDateRange.EndtDate = Convert.ToDateTime("13-Aug-2018");
lstDate.Add(aDateRange);
return lstDate;
}
/**List of Dates Here - Ends**/
Unfortunately this doesn't return the desired output though the list has the specific date.
Update 1:
Expected Output - FromDate and ToDate values are stored in the list.
FromDate ToDate
10-AUG-2018 13-AUG-2018
**in the date range**
FromDate ToDate
13-AUG-2018 16-AUG-2018
**in the date range** //As 13 is the end date in the given list
FromDate ToDate
8-AUG-2018 10-AUG-2018
**in the date range** //As 10 is the start date in the given list
FromDate ToDate
8-AUG-2018 8-AUG-2018
**not in the date range** //As 10 is the start date in the given list
I'm having a bit of trouble figuring out what it is you're trying to do, to be perfectly honest, and I can't help but feel you're "over-engineering" your solution.
First, a "date range" is just two dates - a staring date and an end date, but your GetDateRange method has 4 dates inside it, which it returns as a list. This is incredibly confusing - and I'm not sure if you're trying to get multiple date-ranges (multiple pairs) or a single date-range out of it. Given that all the dates are one after another, I'm going to assume the latter.
public class DateRange
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
public DateRange GetStaticDateRange()
{
//It seems counterproductive to add all 4 dates here,
//given that these are all one after the other
return new DateRange
{
StartDate = new DateTime(2018, 7, 10),
EndDate = new DateTime(2018, 7, 13)
};
//Obviously this can be modified as needed to return whatever combination of
//start-end dates you want, but this method will only ever return ONE range
//However, this method could just as well accept parameters and / or access other resources
}
public bool IsInDateRange(DateTime dateToCheck, DateRange targetRange)
{
//An argument can be made to use non-encompassing comparisons for both checks
//depending on your requirements
return dateToCheck >= targetRange.StartDate && dateToCheck <= targetRange.EndDate;
}
The above has a simple class for storing a "date-range" (aptly called DateRange), and a sample method which checks if a given DateTime is valid inside a specific DateRange.
EDIT:
OK, so from your updated question it seems like you're trying to find if two date-ranges overlap (at all).
In which case, the code below should help.
public static bool DateRangesOverlap(DateRange range1, DateRange range2)
{
return (range1.StartDate >= range2.StartDate && range1.StartDate <= range2.EndDate) ||
(range1.EndDate >= range2.StartDate && range1.EndDate <= range2.EndDate);
}
Here's a working example on .NET Fiddle with your test cases. Note that I'm still using the DateRange class as defined above with a constructor added for brevity.
Also please note that the DateRange class has no sanity-check for the start and end parameters, and it's possible to create a DateRange with the two values reversed (i.e. start > end) which, obviously, would cause errors. But this is just an example so implementations of these things I leave to you. ;)
You have a few bugs in your code.
For example in the GetDateRange(), you are adding only one date to the range, and its date will set to 13-Aug-2018, so that is one thing you need to fix, and if your goal is to find a date in a range of dates, you can use Linq. To compare ranges, I also suggest use DateTime.CompareTo Method. See the code below for corrections of your errors:
public static bool RangeContainsDate(DateTime queriedDateTime)
{
var queriedDateRange = new DateRange { Date = queriedDateTime };
List<DateRange> dates = GetDateRange();
return dates.Where(d => d.CompareTo(queriedDateRange) == 0).Any();
}
/**List of Dates Here - Starts**/
public static List<DateRange> GetDateRange()
{
List<DateRange> lstDate = new List<DateRange>();
DateRange aDateRange1 = new DateRange();
aDateRange1.Date = Convert.ToDateTime("10-Aug-2018");
lstDate.Add(aDateRange1);
DateRange aDateRange2 = new DateRange();
aDateRange2.Date = Convert.ToDateTime("11-Aug-2018");
lstDate.Add(aDateRange2);
DateRange aDateRange3 = new DateRange();
aDateRange3.Date = Convert.ToDateTime("12-Aug-2018");
lstDate.Add(aDateRange3);
DateRange aDateRange4 = new DateRange();
aDateRange4.Date = Convert.ToDateTime("13-Aug-2018");
lstDate.Add(aDateRange4);
return lstDate;
}
}
}
public class DateRange : IComparable<DateRange>
{
public DateTime Date { set; get; }
public int CompareTo(DateRange other)
{
if (ReferenceEquals(other, null))
{
return -1;
}
return DateTime.Compare(Date, other.Date);
}
}
private void btnClick_Click(object sender, EventArgs e)
{
//DateTime theFromDate = dateTimePicker1.Value;
DateTime theToDate = dateTimePicker2.Value;
List<DateRange> lstRange1 = GetDateRange();
List<DateRange> lstRange2 = GetDateRange();
var result = lstRange1.Any(x => x.date >= theToDate && lstRange2.Any(y => y.date < theToDate));
if (result)
{
MessageBox.Show(theToDate.ToString("dd-MMM-yyyy") + " in the date range!");
}
else
{
MessageBox.Show(theToDate.ToString("dd-MMM-yyyy") + " not in the date range!");
}
}
public List<DateRange> GetDateRange()
{
List<DateRange> lstDate = new List<DateRange>();
lstDate.Add(new DateRange { date = Convert.ToDateTime("10-Aug-2018") });
lstDate.Add(new DateRange { date = Convert.ToDateTime("11-Aug-2018") });
lstDate.Add(new DateRange { date = Convert.ToDateTime("12-Aug-2018") });
lstDate.Add(new DateRange { date = Convert.ToDateTime("13-Aug-2018") });
return lstDate;
}

Perform calculations on a set using only LINQ

I'd like to perform the following using only LINQ.
I have a list of time sheet entries with user's in and out times. The class looks like this:
public class TimeSheetLog
{
public Guid EmployeeId { get; set; }
public DateTime ClockInTimeStamp { get; set; }
public DateTime ClockOutTimeStamp { get; set; }
}
I'm passing a List<TimeSheetLog>() which contains all logs from the beginning of the year to date.
I'm trying to calculate the total work time -- regardless of employee -- for the month of January. Please also notice that I have a function named GetTimeDifferenceInMinutes() which calculates the number of minutes between two date/time values.
Here's what I currently have but I feel the whole thing can be done using LINQ only.
public static int GetTotalTimeWorked(List<TimeSheetLog> logs, DateTime startDate, DateTime endDate)
{
// I'm passing 1/1/2018 for startDate and 1/31/2018 for endDate to this function
var totalTimeWorkedInMinutes = 0;
var januaryLogs = logs.Where(x => x.ClockInTimeStamp >= startDate &&
x.ClockOutTimeStamp <= endDate);
foreach(var item in januaryLogs)
{
totalTimeWorkedInMinutes += GetTimeDifferenceInMinutes(item.ClockInTimeStamp, itemClockOutTimeStamp);
}
return totalTimeWorkedInMinutes;
}
var logsFilteredByDate = logs.Where(x => x.ClockInTimeStamp >= startDate &&
x.ClockOutTimeStamp <= endDate);
var totalTimeWorkedInMinutes = logsFilteredByDate.Sum(x =>
GetTimeDifferenceInMinutes(x.ClockInTimeStamp, x.ClockOutTimeStamp));
Or, to combine it all into one query, which is unnecessary and harder to read,
var totalTimeWorkedInMinutes = logs.Where(x => x.ClockInTimeStamp >= startDate &&
x.ClockOutTimeStamp <= endDate)
.Sum(x =>
GetTimeDifferenceInMinutes(x.ClockInTimeStamp, x.ClockOutTimeStamp));
you need sum
var tot = januaryLogs.Sum(item=>GetTimeDifferenceInMinutes(item.ClockInTimeStamp, itemClockOutTimeStamp));
Couldn't you do the Where with a Sum and do DateTime Subtract in the Sum, so
decimal total = logs.Where(x => x.ClockInTimeStamp >= startDate && x.ClockOutTimeStamp <= endDate).Sum(x.ClockOutTimeStamp.Subtract(x.ClockInTimeStamp).TotalMinutes);
The problem seems easy until you realize that a time sheet can span months. So if someone clocked in on January 31st and clocked out on February 1st, you have to count partial timesheets, to do it right.
Here is my solution:
public static class ExtensionMethods
{
static public double TotalMinutes(this IEnumerable<TimeSheetLog> input, DateTime startPeriod, DateTime endPeriod)
{
return TimeSpan.FromTicks
(
input
.Where( a=>
a.ClockOutTimeStamp >= startPeriod &&
a.ClockInTimeStamp <= endPeriod
)
.Select( a=>
Math.Min(a.ClockOutTimeStamp.Ticks, endPeriod.Ticks) -
Math.Max(a.ClockInTimeStamp.Ticks, startPeriod.Ticks)
)
.Sum()
)
.TotalMinutes;
}
}
Logic:
Find all timesheets that overlap at least partially with the period of interest.
Compute the start time as either the clock in time or the period start time, whichever is later.
Compute the end time as either the clock out time or the period end time, whichever is earlier.
Take the difference of the start and end time as ticks. Sum() these.
To do all this math, we convert all the timestamps to Ticks, since you can't take a Max() of two DateTimes. We can add ticks up just fine, then convert the total back into minutes before returning.
Test program (notice the third timesheet spans both January and February):
public class Program
{
static public List<TimeSheetLog> testData = new List<TimeSheetLog>
{
new TimeSheetLog
{
ClockInTimeStamp = DateTime.Parse("1/1/2018 9:00 am"),
ClockOutTimeStamp = DateTime.Parse("1/1/2018 5:00 pm")
},
new TimeSheetLog
{
ClockInTimeStamp = DateTime.Parse("1/2/2018 9:00 am"),
ClockOutTimeStamp = DateTime.Parse("1/2/2018 5:00 pm")
},
new TimeSheetLog
{
ClockInTimeStamp = DateTime.Parse("1/31/2018 6:00 pm"),
ClockOutTimeStamp = DateTime.Parse("2/1/2018 9:00 am")
},
new TimeSheetLog
{
ClockInTimeStamp = DateTime.Parse("2/3/2018 9:00 am"),
ClockOutTimeStamp = DateTime.Parse("2/3/2018 5:00 pm")
}
};
public static void Main()
{
var startPeriod = new DateTime(2018, 1, 1);
var endPeriod = new DateTime(2018, 1, 31, 23, 59, 59, 9999);
Console.WriteLine( testData.TotalMinutes(startPeriod, endPeriod).ToString("0.00") );
}
}
Output:
1320.00
...which is correct.
See my code on DotNetFiddle
Another option is to use .Aggregate function.
public static int GetTotalTimeWorked(List<TimeSheetLog> logs, DateTime startDate, DateTime endDate)
{
var totalTimeWorkedInMinutes = 0;
return logs.Where(x => x.ClockInTimeStamp >= startDate && x.ClockOutTimeStamp <= endDate)
.Aggregate(totalTimeWorkedInMinutes, (total, item) => total + GetTimeDifferenceInMinutes(item.ClockInTimeStamp, item.ClockOutTimeStamp));
}

Calculating months between 2 dates, then calculating days in each month

So I'm trying to create a graph by getting the start and end date from the user. I need to count the number of months between the dates, then for each month calculate the number of days and get the name of the month as well.
I've got the number of months between the 2 dates but can't seem to translate the remaining into code. I have a class called MonthRange
public class MonthRange
{
public DateTime startDate { get; set; }
public DateTime endDate { get; set; }
public string monthName { get; set; }
public MonthRange() {
}
public MonthRange(DateTime startDate, DateTime endDate, string monthName) {
this.startDate = startDate;
this.endDate = endDate;
this.monthName = monthName;
}
}
And my method:
private List<MonthRange> GetRangeOfMonthsBetweenDates(DateTime startDate, DateTime endDate) {
List<MonthRange> result = new List<MonthRange>();
int months = (endDate.Year - startDate.Year)*12 + endDate.Month - startDate.month;
foreach(var m in months){
//get the start and end date of that month with the name and add it to the result list.
}
}
The kicker is that if the startDate is midway through the month, then that is what should be saved in the result list, likewise if the endDate is midway of the month, that is what should be saved. I'm a bit lost and would appreciate any help.
Edit: So this is an example of what I'm trying to achieve. I'm trying to create a graph that plots the number of times a person has had food in a given time range.
Edit 2: So i ended up going this way:
private List<MonthRange> GetRangeOfMonthsBetweenDates(DateTime startDate, DateTime endDate) {
List<MonthRange> result = new List<MonthRange>();
DateTime holder = startDate;
var months = (endDate.Year - startDate.Year) * 12 + endDate.Month - startDate.Month;
for (int i = 0; i <= months; i++) {
if (i == 0)
{
result.Add(new MonthRange(startDate, CalculateStartOfMonth(endDate), startDate.ToString("MMM")));
}
else if (i == months)
{
result.Add(new MonthRange(CalculateEndOfMonth(startDate), endDate, endDate.ToString("MMM")));
}
else {
DateTime middleMonth = holder.AddMonths(1);
result.Add(new MonthRange(CalculateStartOfMonth(middleMonth), CalculateEndOfMonth(middleMonth), middleMonth.ToString("MMM")));
}
}
return result;
}
private DateTime CalculateStartOfMonth(DateTime endDate) {
var startOfMonth = new DateTime(endDate.Year, endDate.Month, 1);
return startOfMonth;
}
private DateTime CalculateEndOfMonth(DateTime startDate)
{
var endOfMonth = new DateTime(startDate.Year, startDate.Month, DateTime.DaysInMonth(startDate.Year, startDate.Month));
return endOfMonth;
}
Note: this is not an answer to the question but a suggestion for a different approach. Since the question is about charting number of meals for a person over a period, I think the natural way to approach this (at least for representing the data) is to do it by day, not by month. (Visually, the data may have to be represented by month but it's much easier to first organize the data by days and then render it by month.)
Since the question is rather vague, I made some assumptions about the data. This class could be used to store information about a single day.
class DayEntry
{
public DateTime Date { get; set; }
public string MonthName { get; set; }
public int NumberOfMeals { get; set; }
}
The following function creates a list of DayEntry objects, initialized for each day in the date range:
List<DayEntry> CreateDayEntries(DateTime dateStart, DateTime dateEnd)
{
var dateDiff = dateEnd - dateStart;
var dayCount = (int) Math.Ceiling(dateDiff.TotalDays) + 1;
var dayRange = new List<DayEntry>(dayCount);
for (var i = 0; i < dayCount; i++)
{
var date = dateStart.AddDays(i);
var dayEntry = new DayEntry
{
Date = date,
NumberOfMeals = 0, // TODO
MonthName = date.ToString("MMMM", CultureInfo.CurrentCulture)
};
dayRange.Add(dayEntry);
}
return (dayRange);
}
When the data is prepared for the given date range, it can then be shown in a UI, grouped by month - since each day entry has its own date (with the name of the month it belongs to) and are in order in the list, it's easy to iterate through them and create the UI output.
Based on your MonthRange structure, try this:
private IEnumerable<MonthRange> GetRangeOfMonthsBetweenDates(DateTime startDate, DateTime endDate) {
var start = startDate;
while(start<endDate) {
var end = start.AddMonths(1).AddDays(-start.Day);
yield return new MonthRange(start, end < endDate ? end : endDate, start.ToString("MMMM"));
start = end.AddDays(1);
}
}
For example:
var start = new DateTime(2017,5,16);
var end = new DateTime(2017,7,24);
foreach(var m in GetRangeOfMonthsBetweenDates(start, end)) {
Console.WriteLine("{0}: {1:dd/MM/yyyy}-{2:dd/MM/yyyy}", m.monthName, m.startDate, m.endDate);
}
Should print:
May: 16/05/2017-31/05/2017
June: 01/06/2017-30/06/2017
July: 01/07/2017-24/07/2017
What i would suggest is for you to convert everything into days first.
eg. x= 361 ; // total number of days
then from there you can split them up into months and years .
months = x/30; // find months
days = x%30; // find the remainder of the days from the month
years = months/12; // find years
This way its much easier to calculate.

ASP.NET MVC Filter datetime by weeks

I've got a Web API and a Get method, returning a query:
var query = from results in context.Table
where results.Date>= startDate && results.Date <= endDate
select new
{
Week = { this is where I need a method to group by weeks },
Average = results.Where(x => x.Number).Average()
}
return query.ToList();
I want to calculate the average for each 7 days (that being the first week).
Example:
Average 1 ... day 7 (Week 1)
Average 2 ... day 14 (Week 2)
How can I do that? Being given an interval of datetimes, to filter it by weeks (not week of year)
Try this (not tested with tables)
var avgResult = context.QuestionaireResults
.Where(r => (r.DepartureDate >= startDate && r.DepartureDate <= endDate)).ToList()
.GroupBy( g => (Decimal.Round(g.DepartureDate.Day / 7)+1))
.Select( g => new
{
Week = g.Key,
Avg = g.Average(n => n.Number)
});
You will need to group by the number of days, since a reference date, divided by 7, so
.GroupBy(x => Math.Floor(((x.DepartureDate - new DateTime(1980,1,1)).TotalDays + 2) / 7))
Subtracting "Jan 1, 1980" from your departure date, gives you a TimeSpan object with the difference between the two dates. The TotalDays property of that timespan gives you timespan in days. Adding 2 corrects for the fact that "Jan 1, 1980" was a Tuesday. Dividing by 7 gives you the number of weeks since then. Math.Floor rounds it down, so that you get a consistent integer for the week, given any day of the week or portion of days within the week.
You could simplify a little by picking a reference date that is a Sunday (assuming that is your "first day of the week"), so you dont have to add 2 to correct. Like so:
.GroupBy(x => Math.Floor(((x.DepartureDate - new DateTime(1979,12,30)).TotalDays) / 7))
If you are sure that your data all falls within a single calendar year, you could maybe use the Calendar.GetWeekOfYear method to figure out the week, but I am not sure it would be any simpler.
Why not write a stored procedure, I think there may be some limitations on your flexibility using Linq because of the idea that normally the GroupBy groups by value (the value of the referenced "thing") so you can group by State, or Age, but I guess you can Group week... (new thought)
Add a property called EndOfWeek and for example, the end of this week is (Sunday let's say) then EndOfWeek = 9.2.16 whereas last week was 8.28.16... etc. then you can easily group but you still have to arrange the data.
I know I didn't answer the question but I hope that I sparked some brain activity in an area that allows you to solve the problem.
--------- UPDATED ----------------
simple solution, loop through your records, foreach record determine the EndOfWeek for that record. After this you will now have a groupable value. Easily group by EndOfWeek. Simple!!!!!!!!!!!! Now, #MikeMcCaughan please tell me how this doesn't work? Is it illogical to extend an object? What are you talking about?
------------ HERE IS THE CODE ----------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SandboxConsole
{
class Program
{
static void Main(string[] args)
{
var t = new Transactions();
List<Transactions> transactions = t.GetTransactions();
// Now let's add a Weeks end date so we can determine the average per week
foreach(var transaction in transactions)
{
var transactionDayOfWeek = transaction.TransactionDate;
int daysUntilEndOfWeek_Sat = ((int)DayOfWeek.Saturday - (int)transactionDayOfWeek.DayOfWeek + 7) % 7;
transaction.Newly_Added_Property_To_Group_By_Week_To_Get_Averages = transactionDayOfWeek.AddDays(daysUntilEndOfWeek_Sat).ToShortDateString();
//Console.WriteLine("{0} {")
}
foreach(var weekEnd in transactions.GroupBy(tt => tt.Newly_Added_Property_To_Group_By_Week_To_Get_Averages))
{
decimal weekTotal = 0;
foreach(var trans in weekEnd)
{
weekTotal += trans.Amount;
}
var weekAverage = weekTotal / 7;
Console.WriteLine("Week End: {0} - Avg {1}", weekEnd.Key.ToString(), weekAverage.ToString("C"));
}
Console.ReadKey();
}
}
class Transactions
{
public int Id { get; set; }
public string SomeOtherProp { get; set; }
public DateTime TransactionDate { get; set; }
public decimal Amount { get; set; }
public string Newly_Added_Property_To_Group_By_Week_To_Get_Averages { get; set; }
public List<Transactions> GetTransactions()
{
var results = new List<Transactions>();
for(var i = 0; i<100; i++)
{
results.Add(new Transactions
{
Id = i,
SomeOtherProp = "Customer " + i.ToString(),
TransactionDate = GetRandomDate(i),
Amount = GetRandomAmount()
});
}
return results;
}
public DateTime GetRandomDate(int i)
{
Random gen = new Random();
DateTime startTime = new DateTime(2016, 1, 1);
int range = (DateTime.Today - startTime).Days + i;
return startTime.AddDays(gen.Next(range));
}
public int GetRandomAmount()
{
Random rnd = new Random();
int amount = rnd.Next(1000, 10000);
return amount;
}
}
}
------------ OUTPUT ---------------
Sample Output

Find date ranges from a collection of dates C#

Simple question. I have an ordered collection of dates. They are UK dates btw
01/01/10
01/02/10
01/03/10
01/04/10
02/04/10
03/04/10
04/04/10
And I want to convert this into a collection of date ranges
01/01/10 -> 01/01/10
01/02/10 -> 01/02/10
01/03/10 -> 01/03/10
01/04/10 -> 04/04/10
Just to clarify, I'm trying to convert any consecutive dates into a range. so the first 3 dates are stand alone and the last 4 get converted into a range 1st of April to 4th of April.
Now I can do this using loops but it's not very elegant. Does any one have any solutions out there that are?
Thanks
Given that you want to determine ranges of consecutive date ranges, I think your only option is, as you say a loop. You can do it in a single pass though, and put it in an extension method so it'll operate on any IList<DateTime>, for example:
// purely an example, chances are this will have actual, y'know logic in live
public class DateRange
{
private List<DateTime> dates = new List<DateTime>();
public void Add(DateTime date)
{
this.dates.Add(date);
}
public IEnumerable<DateTime> Dates
{
get { return this.dates; }
}
}
public static IEnumerable<DateRange> GetRanges(this IList<DateTime> dates)
{
List<DateRange> ranges = new List<DateRange>();
DateRange currentRange = null;
// this presumes a list of dates ordered by day, if not then the list will need sorting first
for( int i = 0; i < dates.Count; ++i )
{
var currentDate = dates[i];
if( i == 0 || dates[i - 1] != currentDate.AddDays(-1))
{
// it's either the first date or the current date isn't consecutive to the previous so a new range is needed
currentRange = new DateRange();
ranges.Add(currentRange);
}
currentRange.Add(currentDate);
}
return ranges;
}
You could also make it even more generic by passing in an IEnumerable<DateTime>:
public static IEnumerable<DateRange> GetRanges(this IEnumerable<DateTime> dates)
{
List<DateRange> ranges = new List<DateRange>();
DateRange currentRange = null;
DateTime? previousDate = null;
// this presumes a list of dates ordered by day, if not then the list will need sorting first
foreach( var currentDate in dates )
{
if( previousDate == null || previousDate.Value != currentDate.AddDays(-1) )
{
// it's either the first date or the current date isn't consecutive to the previous so a new range is needed
currentRange = new DateRange();
ranges.Add(currentRange);
}
currentRange.Add(currentDate);
previousDate = currentDate;
}
return ranges;
}
dates.Aggregate(new List<DateRange>(), (acc, dt) =>
{
if (acc.Count > 0 && acc.Last().d2 == dt.AddDays(-1))
acc[acc.Count - 1].d2 = dt;
else
acc.Add(new DateRange(dt, dt));
return acc;
}
);
where DateRange is a class like this:
class DateRange
{
public DateTime d1, d2;
public DateRange(DateTime d1, DateTime d2)
{
this.d1 = d1;
this.d2 = d2;
}
}
var stringDates = new List<string> {"01/09/10", "31/08/10", "01/01/10"};
var dates = stringDates.ConvertAll(DateTime.Parse);
dates.Sort();
var lastDateInSequence = new DateTime();
var firstDateInSequence = new DateTime();
foreach (var range in dates.GroupBy(
d => { if ((d - lastDateInSequence).TotalDays != 1)
firstDateInSequence = d;
lastDateInSequence = d;
return firstDateInSequence;
}))
{
var sb = new StringBuilder();
sb.Append(range.First().ToShortDateString());
sb.Append(" => ");
sb.Append(range.Last().ToShortDateString());
Console.WriteLine(sb.ToString());
}

Categories

Resources