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));
}
Related
I've got an ASP.net C# application which creates a list of all the weeks in a given year. e.g. selected year 2019, and will produce 31/12/2019 to 06/01/2019 and so on. see attached image.
To produce this I am borrowing some code from an example I found on Stack Overflow here
Now I also have another list containing dates in the format dd/MM/yyyy, this is generated from an XML file, so I wanted to only show the weeks that match dates in the weeks of the year list and populate the drop down list when a date in my XML generated list is contained within it.
For example if I had a full week or even a day in my XML generated list which fell between the 31/12/2018 to 06/01/2019 I want to show it in the drop down list.
Similarly if the XML generated list doesn't contain at least a day from that week then don't show it.
I've pasted the code I used to get the weeks of a given year below.
I'm not sure of any easy way to compare both lists. Any help would be greatly appreciated.
public List<string> FetchWeeks(int year)
{
List<string> weeks = new List<string>();
DateTime startDate = new DateTime(year, 1, 1);
startDate = startDate.AddDays(1 - (int)startDate.DayOfWeek);
DateTime endDate = startDate.AddDays(6);
while (startDate.Year < 1 + year)
{
weeks.Add(string.Format("{0:dd/MM/yyyy} to {1:dd/MM/yyyy}", startDate, endDate));
startDate = startDate.AddDays(7);
endDate = endDate.AddDays(7);
}
//DropDownList1.Items.Add(weeks);
return weeks;
}
If were trying to compare lists to determine a set of valid weeks, I would try to determine an absolute week index and use that in my work. Since weeks are not impacted by things like leap years or other date oddities, we can just count in 7-day intervals from the beginning of a known of date range. Forgive me if my C# is rusty, but something to the effect of:
public int ToWeekIndex(DateTime date)
{
// Takes any date and maps it to a value that represents the week it resides in.
Timespan ts = date - DateTime.MinValue // Monday, January 1, 0001;
return ts.Days / 7; // Integer divide, drops the remainder.
}
public DateTime FromWeekIndex(int weekIndex)
{
// Takes a week index and returns the Monday from it.
Timespan ts = new Timespan(weekIndex * 7, 0, 0, 0); // Days, hours, minutes, seconds
return DateTime.MinValue + ts;
}
Then to build out your weeks, you could do something to the effect of pseudocode:
all_weeks = []
for date in January 1, 2019 to December 31, 2019 step 7 days:
week_index = ToWeekIndex(date)
week_start = FromWeekIndex(week_index)
week_end = week_start + 7 days - 1 second
all_weeks += [week_start, week_end]
instead of a list of string for dates, use a list of object that contains the week dates and a boolean defaulted to false.
public class WeekObj
{
public string Week { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public bool IsValid { get; set; }
};
List<WeekObj> weeks= new WeekObj();
weeks.add(new WeekObj { "week string", startDate, endDate, false });
Loop through your list of days, and for each day go through the list of weeks and set it to true if the day is between startDate and endDate (which is now in DateTime obj), do that for false dates, no need to recompare true dates.
public static bool Between(DateTime input, DateTime date1, DateTime date2)
{
return (input >= date1 && input <= date2);
}
This is the weeks in the Year data you already have -
class WeekData
{
public DateTime WeekStartDate { get; set; }
public DateTime WeekEndDate { get; set; }
public int WeekStartDay //Gets Day in the year for the Week Start Date
{
get { return WeekStartDate.DayOfYear; }
}
public int WeekEndDay //Gets Day in the year for the Week End Date
{
get { return WeekEndDate.DayOfYear; }
}
}
Dummy WeeksInTheYear data
List<WeekData> weeks = new List<WeekData>
{
new WeekData{WeekStartDate = new DateTime(2019,10,6), WeekEndDate = new DateTime(2019,10,12)},
new WeekData{WeekStartDate = new DateTime(2019,10,13), WeekEndDate = new DateTime(2019,10,19)},
new WeekData{WeekStartDate = new DateTime(2019,10,20), WeekEndDate = new DateTime(2019,10,26)},
new WeekData{WeekStartDate = new DateTime(2019,10,27), WeekEndDate = new DateTime(2019,11,2)}
};
Dummy Dates from the XML feed
List<DateTime> xmlDates = new List<DateTime> { new DateTime(2019, 11, 1), new DateTime(2019, 10, 12), new DateTime(2019, 10, 31) };
Filtering
var weeksINeed = new List<WeekData>();
foreach (var date in xmlDates)
{
var weekINeed = weeks.Where(x => x.WeekStartDay <= date.DayOfYear && x.WeekEndDay >= date.DayOfYear)
.FirstOrDefault();
if (!weeksINeed.Any(x => x.WeekStartDay == weekINeed.WeekStartDay))
{
weeksINeed.Add(weekINeed);
}
}
Output -
foreach (var weekdata in weeksINeed.OrderBy(x=>x.WeekStartDay))
{
Console.WriteLine($"WeekStartDate - {weekdata.WeekStartDate} WeekEndDate - {weekdata.WeekEndDate}");
}
Using some extension functions and LINQ, you can just generate the list directly from the XML Date List<string>.
First, an IEnumerable<> extension to select distinct by a lambda function:
public static class IEnumerableExt {
public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> src, Func<T, TKey> keySelector, IEqualityComparer<TKey> comparer = null) {
var seenKeys = new HashSet<TKey>(comparer);
foreach (var e in src)
if (seenKeys.Add(keySelector(e)))
yield return e;
}
}
Then some calendar extensions using the built-in ISOWeek methods to get the week of year (Based on your week date ranges, I assume you are using ISO 8601 Weeks):
public static class CalendarExt {
public static int GetISO8601WeekOfYear(this DateTime aDate) => ISOWeek.GetWeekOfYear(aDate);
public static DateTime FirstDateOfYear(this DateTime d) => new DateTime(d.Year, 1, 1);
public static DateTime FirstDateOfISO8601Week(this DateTime aDate) => aDate.AddDays(-(((int)aDate.DayOfWeek + 6) % 7));
public static DateTime LastDateofISO8601Week(this DateTime aDate) => aDate.FirstDateOfISO8601Week().AddDays(6);
public static DateTime FirstDateOfISO8601Week(int year, int weekNum) => ISOWeek.ToDateTime(year, weekNum, DayOfWeek.Monday);
public static DateTime LastDateofISO8601Week(int year, int weekNum) => FirstDateOfISO8601Week(year, weekNum).AddDays(6);
// for .Net without ISOWeek
//public static DateTime FirstDateOfISO8601Week(this DateTime aDate) => aDate.AddDays(-(((int)aDate.DayOfWeek + 6) % 7));
//public static int GetISO8601WeekOfYear(this DateTime aDate) =>
// CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(aDate.AddDays(DayOfWeek.Monday <= aDate.DayOfWeek && aDate.DayOfWeek <= DayOfWeek.Wednesday ? 3 : 0), CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}
Finally, given your list of string dates from XML in xmlDateStrings, you can compute the week ranges list:
var currentCulture = CultureInfo.CurrentCulture;
var ans = xmlDateStrings.Select(ds => DateTime.ParseExact(ds, "dd/MM/yyyy", currentCulture))
.DistinctBy(d => d.GetISO8601WeekOfYear())
.OrderBy(d => d) // assume XML is unsorted
.Select(d => $"{d.FirstDateOfISO8601Week().ToString("dd/MM/yyyy")} to {d.LastDateofISO8601Week().ToString("dd/MM/yyyy")}")
.ToList();
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.
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;
}
}
I have DateStart, DateEnd Periodicity, TypePeriodicity fields.
We have a query:
var result = Events.Where(e => e.DateStart <=today && e.DateEnd >= today).ToList();
I want that this query to check Periodicity.
For example:
name - record1
DateStart = 2012-02-02
DateEnd = 2012-03-31
Periodicity = 2
TypePeriodicity = 1 ( it's mean a week, may be also day = 0, month=2):
I want the following, if current date equals:
2,3,4,5 February - return `record1`
6,7,8..12 - not return, because TypePeriodicity = 1 and Periodicity = 2, which means every 2 weeks
13..19 - return `record1`
20..26 - not return
and so on until `DateEnd`
Thanks.
PS. Maybe not LINQ, but simple method that recieve result as parameter.
Here is something to get you started:
You could define a DateEvaluator delegate like so:
delegate bool DateEvaluator(DateTime startDate, DateTime endDate, DateTime dateToCheck, int periodicity);
The purpose of the delegate would be to evaluate for a given periodicity type if a date should be considered as within range. We would have hence 3 date evaluators.
One for each period type: Lets call them dayPeriodicityChecker, weekPeriodicityChecker and monthPeriodicityChecker
Our dayPeriodicityChecker is straightforward:
DateEvaluator dayPeriodicityChecker = (startDate, endDate, dateToCheck, periodicity) =>
{
if ((dateToCheck < startDate) || (dateToCheck > endDate))
return false;
TimeSpan dateDiff = dateToCheck - startDate;
return dateDiff.Days % periodicity == 0;
};
Our weekPeriodicityChecker needs to account for the start day of week, so the start date would need to be adjusted to the date in which the startDate week actually starts:
DateEvaluator weekPeriodicityChecker = (startDate, endDate, dateToCheck, periodicity) =>
{
if ((dateToCheck < startDate) || (dateToCheck > endDate))
return false;
DateTime adjustedStartDate = startDate.AddDays(-(int)startDate.DayOfWeek + 1);
TimeSpan dateDiff = dateToCheck - adjustedStartDate;
return (dateDiff.Days / 7) % periodicity == 0;
};
Our monthPeriodicityChecker needs to cater for months with a variable number of days:
DateEvaluator monthPeriodicityChecker dateToCheck, periodicity) =>
{
if ((dateToCheck < startDate) || (dateToCheck > endDate))
return false;
int monthDiff = 0;
while (startDate.AddMonths(1) < dateToCheck)
{
monthDiff++
// i'm sure there is a speedier way to calculate the month difference, but this should do for the purpose of this example
}
return (monthDiff - 1) % periodicity == 0;
};
Once you have all your date evaluators defined you could put them in an array like so:
DateEvaluator[] dateEvaluators = new DateEvaluator[]
{
dayPeriodicityChecker,
weekPeriodicityChecker,
monthPeriodicityChecker
};
This will allow you to do :
int periodicityType = 0; // or 1=week or 2=months
bool isDateIn = dateEvaluators[periodicityType ](startDate, endDate, dateTocheck, Periodicity)
So lets test this:
PeriodicityEvent pEvent = new PeriodicityEvent
{
Name = "record1",
DateStart = new DateTime(2012, 02, 02),
DateEnd = new DateTime(2012, 03, 31),
PeriodicityType = 1,
Periodicity = 2
};
DateTime baseDate = new DateTime(2012, 02, 01);
for (int i = 0; i < 30; i++)
{
DateTime testDate = baseDate.AddDays(i);
if (dateEvaluators[pEvent.PeriodicityType](pEvent.DateStart, pEvent.DateEnd, testDate, pEvent.Periodicity))
{
Console.WriteLine("{0} is in", testDate.ToString("dd MMM"));
}
else
{
Console.WriteLine("{0} is out", testDate.ToString("dd MMM"));
}
}
This will produce the desired output as below:
To use you would simply do:
Events.Where(e => dateEvaluators[e.PeriodType](e.DateStart, e.DateEnd, today, e.Periodicity).ToList();
Good luck!
I'm not even sure how to do this without using some horrible for loop/counter type solution. Here's the problem:
I'm given two dates, a start date and an end date and on a specified interval I need to take some action. For example: for every date between 3/10/2009 on every third day until 3/26/2009 I need to create an entry in a List. So my inputs would be:
DateTime StartDate = "3/10/2009";
DateTime EndDate = "3/26/2009";
int DayInterval = 3;
and my output would be a list that has the following dates:
3/13/2009
3/16/2009
3/19/2009
3/22/2009
3/25/2009
So how the heck would I do something like this? I thought about using a for loop that would iterate between every day in the range with a separate counter like so:
int count = 0;
for(int i = 0; i < n; i++)
{
count++;
if(count >= DayInterval)
{
//take action
count = 0;
}
}
But it seems like there could be a better way?
Well, you'll need to loop over them one way or the other. I prefer defining a method like this:
public IEnumerable<DateTime> EachDay(DateTime from, DateTime thru)
{
for(var day = from.Date; day.Date <= thru.Date; day = day.AddDays(1))
yield return day;
}
Then you can use it like this:
foreach (DateTime day in EachDay(StartDate, EndDate))
// print it or whatever
In this manner you could hit every other day, every third day, only weekdays, etc. For example, to return every third day starting with the "start" date, you could just call AddDays(3) in the loop instead of AddDays(1).
I have a Range class in MiscUtil which you could find useful. Combined with the various extension methods, you could do:
foreach (DateTime date in StartDate.To(EndDate).ExcludeEnd()
.Step(DayInterval.Days())
{
// Do something with the date
}
(You may or may not want to exclude the end - I just thought I'd provide it as an example.)
This is basically a ready-rolled (and more general-purpose) form of mquander's solution.
For your example you can try
DateTime StartDate = new DateTime(2009, 3, 10);
DateTime EndDate = new DateTime(2009, 3, 26);
int DayInterval = 3;
List<DateTime> dateList = new List<DateTime>();
while (StartDate.AddDays(DayInterval) <= EndDate)
{
StartDate = StartDate.AddDays(DayInterval);
dateList.Add(StartDate);
}
Code from #mquander and #Yogurt The Wise used in extensions:
public static IEnumerable<DateTime> EachDay(DateTime from, DateTime thru)
{
for (var day = from.Date; day.Date <= thru.Date; day = day.AddDays(1))
yield return day;
}
public static IEnumerable<DateTime> EachMonth(DateTime from, DateTime thru)
{
for (var month = from.Date; month.Date <= thru.Date || month.Month == thru.Month; month = month.AddMonths(1))
yield return month;
}
public static IEnumerable<DateTime> EachDayTo(this DateTime dateFrom, DateTime dateTo)
{
return EachDay(dateFrom, dateTo);
}
public static IEnumerable<DateTime> EachMonthTo(this DateTime dateFrom, DateTime dateTo)
{
return EachMonth(dateFrom, dateTo);
}
1 Year later, may it help someone,
This version includes a predicate, to be more flexible.
Usage
var today = DateTime.UtcNow;
var birthday = new DateTime(2018, 01, 01);
Daily to my birthday
var toBirthday = today.RangeTo(birthday);
Monthly to my birthday, Step 2 months
var toBirthday = today.RangeTo(birthday, x => x.AddMonths(2));
Yearly to my birthday
var toBirthday = today.RangeTo(birthday, x => x.AddYears(1));
Use RangeFrom instead
// same result
var fromToday = birthday.RangeFrom(today);
var toBirthday = today.RangeTo(birthday);
Implementation
public static class DateTimeExtensions
{
public static IEnumerable<DateTime> RangeTo(this DateTime from, DateTime to, Func<DateTime, DateTime> step = null)
{
if (step == null)
{
step = x => x.AddDays(1);
}
while (from < to)
{
yield return from;
from = step(from);
}
}
public static IEnumerable<DateTime> RangeFrom(this DateTime to, DateTime from, Func<DateTime, DateTime> step = null)
{
return from.RangeTo(to, step);
}
}
Extras
You could throw an Exception if the fromDate > toDate, but I prefer to return an empty range instead []
DateTime startDate = new DateTime(2009, 3, 10);
DateTime stopDate = new DateTime(2009, 3, 26);
int interval = 3;
for (DateTime dateTime=startDate;
dateTime < stopDate;
dateTime += TimeSpan.FromDays(interval))
{
}
DateTime begindate = Convert.ToDateTime("01/Jan/2018");
DateTime enddate = Convert.ToDateTime("12 Feb 2018");
while (begindate < enddate)
{
begindate= begindate.AddDays(1);
Console.WriteLine(begindate + " " + enddate);
}
According to the problem you can try this...
// looping between date range
while (startDate <= endDate)
{
//here will be your code block...
startDate = startDate.AddDays(1);
}
thanks......
DateTime startDate = new DateTime(2009, 3, 10);
DateTime stopDate = new DateTime(2009, 3, 26);
int interval = 3;
while ((startDate = startDate.AddDays(interval)) <= stopDate)
{
// do your thing
}
Here are my 2 cents in 2020.
Enumerable.Range(0, (endDate - startDate).Days + 1)
.ToList()
.Select(a => startDate.AddDays(a));
You can use the DateTime.AddDays() function to add your DayInterval to the StartDate and check to make sure it is less than the EndDate.
You might consider writing an iterator instead, which allows you to use normal 'for' loop syntax like '++'. I searched and found a similar question answered here on StackOverflow which gives pointers on making DateTime iterable.
you have to be careful here not to miss the dates when in the loop a better solution would be.
this gives you the first date of startdate and use it in the loop before incrementing it and it will process all the dates including the last date of enddate hence <= enddate.
so the above answer is the correct one.
while (startdate <= enddate)
{
// do something with the startdate
startdate = startdate.adddays(interval);
}
you can use this.
DateTime dt0 = new DateTime(2009, 3, 10);
DateTime dt1 = new DateTime(2009, 3, 26);
for (; dt0.Date <= dt1.Date; dt0=dt0.AddDays(3))
{
//Console.WriteLine(dt0.Date.ToString("yyyy-MM-dd"));
//take action
}
Iterate every 15 minutes
DateTime startDate = DateTime.Parse("2018-06-24 06:00");
DateTime endDate = DateTime.Parse("2018-06-24 11:45");
while (startDate.AddMinutes(15) <= endDate)
{
Console.WriteLine(startDate.ToString("yyyy-MM-dd HH:mm"));
startDate = startDate.AddMinutes(15);
}
#jacob-sobus and #mquander and #Yogurt not exactly correct.. If I need the next day I wait 00:00 time mostly
public static IEnumerable<DateTime> EachDay(DateTime from, DateTime thru)
{
for (var day = from.Date; day.Date <= thru.Date; day = day.NextDay())
yield return day;
}
public static IEnumerable<DateTime> EachMonth(DateTime from, DateTime thru)
{
for (var month = from.Date; month.Date <= thru.Date || month.Year == thru.Year && month.Month == thru.Month; month = month.NextMonth())
yield return month;
}
public static IEnumerable<DateTime> EachYear(DateTime from, DateTime thru)
{
for (var year = from.Date; year.Date <= thru.Date || year.Year == thru.Year; year = year.NextYear())
yield return year;
}
public static DateTime NextDay(this DateTime date)
{
return date.AddTicks(TimeSpan.TicksPerDay - date.TimeOfDay.Ticks);
}
public static DateTime NextMonth(this DateTime date)
{
return date.AddTicks(TimeSpan.TicksPerDay * DateTime.DaysInMonth(date.Year, date.Month) - (date.TimeOfDay.Ticks + TimeSpan.TicksPerDay * (date.Day - 1)));
}
public static DateTime NextYear(this DateTime date)
{
var yearTicks = (new DateTime(date.Year + 1, 1, 1) - new DateTime(date.Year, 1, 1)).Ticks;
var ticks = (date - new DateTime(date.Year, 1, 1)).Ticks;
return date.AddTicks(yearTicks - ticks);
}
public static IEnumerable<DateTime> EachDayTo(this DateTime dateFrom, DateTime dateTo)
{
return EachDay(dateFrom, dateTo);
}
public static IEnumerable<DateTime> EachMonthTo(this DateTime dateFrom, DateTime dateTo)
{
return EachMonth(dateFrom, dateTo);
}
public static IEnumerable<DateTime> EachYearTo(this DateTime dateFrom, DateTime dateTo)
{
return EachYear(dateFrom, dateTo);
}
If you convert your dates to OADate you can loop thru them as you would do with any double number.
DateTime startDate = new DateTime(2022, 1, 1);
DateTime endDate = new DateTime(2022, 12, 31);
for (double loopDate = startDate.ToOADate(); loopDate <= endDate.ToOADate(); loopDate++)
{
DateTime selectedDate;
selectedDate = DateTime.FromOADate(loopDate);
}