Writing a small application to calculate interest but the rate changes yearly. Needed to break the range into smaller date ranges when ever it crosses a year boundary. I wrote a little for loop to do it but it's rather clunky. Wondering if there are any built in functions to do this in C# (possible linq). Would basically be looking to return a list of date ranges with the corresponding base year (shortened code for readability).
static void Main(string[] args)
{
var dateStart = DateTime.Parse("2/10/2018");
var dateEnd = DateTime.Parse("3/10/2021");
var years = Years(dateStart, dateEnd);
var baseYear = dateStart.Year;
Console.WriteLine(baseYear);
var loopDateStart = dateStart;
var loopDateEnd = DateTime.Now;
for (int i = 0; i < years + 1; i++)
{
if (i < years) {
loopDateEnd = DateTime.Parse("1/1/" + (baseYear + 1));
Console.WriteLine(loopDateEnd + " ... " + loopDateStart);
Console.WriteLine((loopDateEnd - loopDateStart).Days);
loopDateStart = loopDateEnd;
baseYear++;
}
else {
loopDateEnd = dateEnd;
Console.WriteLine(loopDateEnd + " ... " + loopDateStart);
Console.WriteLine((loopDateEnd - loopDateStart).Days);
}
}
}
public static int Years(DateTime start, DateTime end)
{
return (end.Year - start.Year - 1) +
(((end.Month > start.Month) ||
((end.Month == start.Month) && (end.Day >= start.Day))) ? 1 : 0);
}
Sure, we can use LINQ:
var x = Enumerable.Range(dateStart.Year, (dateEnd.Year-dateStart.Year)+1)
.Select(y => new{
F = new[]{dateStart, new DateTime(y,1,1)}.Max(),
T = new[]{dateEnd, new DateTime(y,12,31)}.Min()
});
It generates an enumerable list of objects that have an F and a T property (from and to) that are your ranges.
It works by using Enumerable.Range to make a list of years: 2018,2019,2020,2021 by starting at 2108 and proceeding for 4 years (2018 to 2018 is one year entry, 2018 to 2021 is 4 year entries)
Then we just turn them into dates using new DateTime(year,amonth,aday) - when were making start dates, amonth and aday are 1 and 1, when making end dates they're 12 and 31
Then we just ask for every year y, "which date is greater, the startdate, or the 1-Jan-y" and "which date is lesser, the enddate or the 31-Dec-y " - for the initial and final date entry it's the startdate and the enddate that are greater and lesser. For other years it's the jan/dec dates. This gives the ranges you want
foreach(var xx in x){
Console.WriteLine(xx.F +" to "+xx.T);
}
2/10/2018 12:00:00 AM to 12/31/2018 12:00:00 AM
1/1/2019 12:00:00 AM to 12/31/2019 12:00:00 AM
1/1/2020 12:00:00 AM to 12/31/2020 12:00:00 AM
1/1/2021 12:00:00 AM to 3/10/2021 12:00:00 AM
If you want to do other work like the number of days between, you can do xx.T-xx.F in the loop, to make a timespan etc
Try:
var start = DateTime.Parse("4/5/2017");
var end = DateTime.Parse("3/1/2019");
DateTime chunkEnd;
for (var chunkStart = start; chunkStart < end; chunkStart = chunkEnd.AddDays(1))
{
var lastDay = new DateTime(chunkStart.Year, 12, 31);
chunkEnd = end > lastDay ? lastDay : end;
var days = (chunkEnd - chunkStart).Days;
Console.WriteLine($"{chunkStart:d} - {chunkEnd:d}; {days} days");
}
Produces:
4/5/2017 - 12/31/2017; 270 days
1/1/2018 - 12/31/2018; 364 days
1/1/2019 - 3/1/2019; 59 days
I came up with the following:
static IEnumerable<(DateTime,DateTime)> ChunkByYear(DateTime start, DateTime end)
{
// Splits <start,end> into chunks each belonging to a different year
while(start <= end)
{
var tempEnd = new DateTime(start.Year, 12, 31);
if(tempEnd >= end ) {
yield return (start, end);
yield break;
}
yield return (start, tempEnd);
start = tempEnd.AddDays(1);
}
}
Here are some results:
4/05/2017 to 3/01/2019:
4/05/2017->31/12/2017
1/01/2018->31/12/2018
1/01/2019->3/01/2019
4/05/2017 to 4/05/2017:
4/05/2017->4/05/2017
31/12/2017 to 31/12/2019:
31/12/2017->31/12/2017
1/01/2018->31/12/2018
1/01/2019->31/12/2019
31/12/2019 to 31/12/2019:
31/12/2019->31/12/2019
31/12/2018 to 1/01/2019:
31/12/2018->31/12/2018
1/01/2019->1/01/2019
Group by years:
using System.Collections.Generic;
using System.Linq;
...
static void Main(string[] args)
{
DateTime dateStart = DateTime.Parse("2/10/2018");
DateTime dateEnd = DateTime.Parse("3/10/2021");
// Group all possible dates by year
foreach(var group in GetDates(dateStart, dateEnd).GroupBy(date => date.Year))
{
Console.WriteLine(group.Key); // The key of the group is year
Console.WriteLine($"{group.Min()} ... {group.Max()}"); // Range: From minimum to maximum, order doesn't matter.
Console.WriteLine($"{group.First()} ... {group.Last()}"); //or Range version 2: From first to last, order matters.
Console.WriteLine(group.Count()); // Count days
}
}
/// <summary>
/// Get all days blindly, might need to pay attention to days on the boundaries
/// </summary>
private static IEnumerable<DateTime> GetDates(DateTime start, DateTime end)
{
// TODO: Check start <= end;
DateTime current = start;
while(current <= end)
{
yield return current;
current = current.AddDays(1);
}
}
Related
How can I select all weekends until the end of the year, with some criteria to be followed?
User input desired Weekend day:
18/12/2021
Software must out:
25/12/2022 (must be ignored)
01/01/2022
08/01/2022 (must be ignored)
15/01/2022
22/01/2022 (must be ignored)
29/01/2022 and so on...
What i have now:
public void GetWeekends() {
var lastWorkedWeekend = dateTimePicker1.Value;
var workedInSunday = checkBox1.Checked;
var list = new List < DateTime > ();
var weekends = GetDaysBetween(lastWorkedWeekend, DateTime.Today.AddDays(365)).Where(d => d.DayOfWeek == DayOfWeek.Saturday);
var selected = true;
for (int i = 0; i < weekends.Count(); i++) {
if (selected == false) {
list.Add(weekends.ElementAt(i));
selected = true;
} else {
selected = false;
}
}
}
I think I'd just scroll the input day forward until it was saturday (or calculate it, but i find the loop more self documenting than casting DayOfWeek to an int and factoring for sunday being 0) then add 14 days repeatedly. This skips over the 25th, etc..
var d = new DateTime(2021, 12, 18);
while(d.DayOfWeek != DayOfWeek.Saturday)
d += TimeSpan.FromDays(1);
while(d.Year == 2021){ //was your 2022 a typo? or maybe make this <= 2022.. I'm not sure what you want there..
d += TimeSpan.FromDays(14);
Console.WriteLine(d);
}
You might prefer DateTime.AddDays()..
Looks to me that you want to get the date of every second weekend from an initial date until the end of the year. In your example, you kinda bleed over to the next year.
public static void Main()
{
var dates = GetDateTimeRange(new DateTime(2021, 12, 18), new DateTime(2023, 1, 1), TimeSpan.FromDays(14));
foreach (var dateTime in dates.Skip(1))
{
Console.WriteLine(dateTime);
}
}
public static IEnumerable<DateTime> GetDateTimeRange(DateTime startingDate, DateTime endDate, TimeSpan interval)
{
var lastDate = startingDate;
while (lastDate < endDate)
{
yield return lastDate;
lastDate = lastDate.Add(interval);
}
}
This returns
01/01/2022 00:00:00
01/15/2022 00:00:00
01/29/2022 00:00:00
02/12/2022 00:00:00
02/26/2022 00:00:00
03/12/2022 00:00:00
03/26/2022 00:00:00
04/09/2022 00:00:00
04/23/2022 00:00:00
05/07/2022 00:00:00
05/21/2022 00:00:00
06/04/2022 00:00:00
06/18/2022 00:00:00
07/02/2022 00:00:00
07/16/2022 00:00:00
07/30/2022 00:00:00
08/13/2022 00:00:00
08/27/2022 00:00:00
09/10/2022 00:00:00
09/24/2022 00:00:00
10/08/2022 00:00:00
10/22/2022 00:00:00
11/05/2022 00:00:00
11/19/2022 00:00:00
12/03/2022 00:00:00
12/17/2022 00:00:00
12/31/2022 00:00:00
One approach is to use a method that creates an enumerable of dates. Once that's done you can use LINQ queries. If creating that enumerable is expensive you could just create one large range encompassing years and re-use it.
Or you might find better performance by just creating the range you need for each query.
public static class DateRanges
{
public static IEnumerable<DateOnly> GetRange(DateOnly start, DateOnly end)
{
for (var date = start; date <= end; date = date.AddDays(1))
{
yield return date;
}
}
}
It's not clear from your code what the criteria is, but this does what you described - all weekends from now to the end of the year.
var today = DateOnly.FromDateTime(DateTime.Today);
var lastDayOfYear = DateOnly.FromDateTime(new DateTime(DateTime.Today.Year, 12, 31));
var dates = DateRanges.GetRange(today, lastDayOfYear);
var weekendsOnly = dates.Where(date =>
date.DayOfWeek == DayOfWeek.Saturday);
Or if you prefer to create one date range and query it repeatedly:
// Big range of dates, 10 years into past and future.
// Create this once and re-use it
var dates = DateRanges.GetRange(
DateOnly.FromDateTime(DateTime.Today.AddYears(-10)),
DateOnly.FromDateTime(DateTime.Today.AddYears(10)));
var today = DateOnly.FromDateTime(DateTime.Today);
var lastDayOfYear = DateOnly.FromDateTime(new DateTime(DateTime.Today.Year, 12, 31));
var weekendsOnly = dates.Where(date =>
date >= today
&& date <= lastDayOfYear
&& date.DayOfWeek == DayOfWeek.Saturday);
In either case if you want every other Saturday you can add
.Where((date, i) => i % 2 == 0);
Or to maintain that readability you could put that in another extension like
public static IEnumerable<T> EveryOther<T>(this IEnumerable<T> source)
{
return source.Where((date, i) => i % 2 == 0);
}
so your query looks like
var everyOtherWeekend = dates
.Where(date =>
date >= today
&& date <= lastDayOfYear
&& date.DayOfWeek == DayOfWeek.Saturday)
.EveryOther();
I wouldn't position this as a better answer than those that iterate over a series of dates. The difference is that instead of having to write a method for any query you can start with a range of dates and then use LINQ to filter it. That makes it a little easier to read and to compose different queries.
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));
}
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 a list of dates that are apart by a month in the sense that all dates are the "First Monday of the month". In some cases months are missing so I need to write a function to determine if all dates are consecutive
So for example if this was the list of dates, the function would return true as all items are the "First Friday of the month" and there are no gaps. This example below would return true.
var date = new DateTime(2013, 1, 4);
var date1 = new DateTime(2013, 2, 1);
var date2 = new DateTime(2013, 3, 1);
var date3 = new DateTime(2013, 4, 5);
var dateArray = new DateTime[]{date, date1, date2, date3};
bool isConsecutive = IsThisListConsecutive(dateArray);
where this example below would return false because, even though they are also all "First Friday of the month", its missing the March 2013 item.
var date = new DateTime(2013, 1, 4);
var date1 = new DateTime(2013, 2, 1);
var date3 = new DateTime(2013, 4, 5);
var dateArray = new DateTime[]{date, date1, date3};
bool isConsecutive = IsThisListConsecutive(dateArray);
so i am trying to figure out the right logic for the IsThisListConsecutive() method:
Here was my first try: (Note I already know upfront that all dates are same day of week and same week of month so the only thing i am looking for is a missing slot)
private bool IsThisListConsecutive(IEnumerable<DateTime> orderedSlots)
{
DateTime firstDate = orderedSlots.First();
int count = 0;
foreach (var slot in orderedSlots)
{
if (slot.Month != firstDate.AddMonths(count).Month)
{
return false;
}
count++;
}
return true;
}
This code above works exept if the list crosses over from one year to another. I wanted to get any advice on a better way to create this function and how that line could be rewritten to deal with dates that cross over years.
So to implement this we'll start with a simple helper method that takes a sequence and returns a sequence of pairs that make up each item with it's previous item.
public static IEnumerable<Tuple<T, T>> Pair<T>(this IEnumerable<T> source)
{
T previous;
using (var iterator = source.GetEnumerator())
{
if (iterator.MoveNext())
previous = iterator.Current;
else
yield break;
while(iterator.MoveNext())
{
yield return Tuple.Create(previous, iterator.Current);
previous = iterator.Current;
}
}
}
We'll also use this simple method to determine if two dates are in the same month:
public static bool AreSameMonth(DateTime first, DateTime second)
{
return first.Year == second.Year
&& first.Month == second.Month;
}
Using that, we can easily grab the month of each date and see if it's the month after the previous month. If it's true for all of the pairs, then we have consecutive months.
private static bool IsThisListConsecutive(IEnumerable<DateTime> orderedSlots)
{
return orderedSlots.Pair()
.All(pair => AreSameMonth(pair.Item1.AddMonths(1), pair.Item2));
}
Note: This is completely untested, and the date checks are probably pretty bad or somewhat redundant, but that’s the best I could come up with right now ^^
public bool AreSameWeekdayEveryMonth(IEnumerable<DateTime> dates)
{
var en = dates.GetEnumerator();
if (en.MoveNext())
{
DayOfWeek weekday = en.Current.DayOfWeek;
DateTime previous = en.Current;
while (en.MoveNext())
{
DateTime d = en.Current;
if (d.DayOfWeek != weekday || d.Day > 7)
return false;
if (d.Month != previous.Month && ((d - previous).Days == 28 || (d - previous).Days == 35))
return false;
previous = d;
}
}
return true;
}
I would recommend looking at the TimeSpan structure. Thanks to operator overload you can get a TimeSpan by substracting two dates and then receive a TimeSpan that expresses the difference between the two dates.
http://msdn.microsoft.com/en-us/library/system.timespan.aspx
okay, your code doesnt work when the years cross over becuase jan 1st may be a monday on one year and a tuesday on the next. If I was doing this, I would first check that
a) they are the same day of the week in each month (use DateTime.DayOfWeek)
b) they are the same week of the month in each month*
use extension method DayOfMonth (see link)
* Calculate week of month in .NET *
(you said you already know a & b to be true so lets go on to the third condition)
c) we have to determine if they are in consecutive months
//order the list of dates & place it into an array for ease of looping
DateTime[] orderedSlots = slots.OrderBy( t => t).ToArray<DateTime>();
//create a variable to hold the date from the previous month
DateTime temp = orderedSlots[0];
for(i= 1; index < orderedSlots.Length; index++)
{
if((orderedSlots[index].Month != temp.AddMonths(1).Month |
orderedSlots[index].Year != temp.AddMonths(1).Year)){
return false;
}
previousDate = orderedSlots[index];
}
return true;
if you need to check conditions a & b as well add change the if statement as follows
if( orderedSlots[index].Month != temp.AddMonths(1).Month |
orderedSlots[index].Year != temp.AddMonths(1).Year) |
orderedSlots[index].DayOfWeek != temp.DayOfWeek |
orderedSlots[index].GetWeekOfMonth != temp.AddMonths(1).GetWeekOfMonth){
return false;
}
remember that to use the get week of month extension method you have to include the code in
Calculate week of month in .NET
I'm sure there are typos as I did this in a text editor.
Well, here is my initial thought on how I would approach this problem.
First, is to define a function that will turn the dates into the ordinal values corresponding to the order in which they should appear.
int ToOrdinal(DateTime d, DateTime baseline) {
if (d.Day <= 7
&& d.DayInWeek == baseline.DayInWeek) {
// Since there is only one "First Friday" a month, and there are
// 12 months in year we can easily compose the ordinal.
// (As per default.kramer's comment, months normalized to [0,11].)
return d.Year * 12 + (d.Month - 1);
} else {
// Was not correct "kind" of day -
// Maybe baseline is Tuesday, but d represents Wednesday or
// maybe d wasn't in the first week ..
return 0;
}
}
var dates = ..;
var baseline = dates.FirstOrDefault();
var ordinals = dates.Select(d => ToOrdinal(d, baseline));
Then, for the dates provided, we end up with ordinal sequences like:
[24156 + 0, 24156 + 1, 24156 + 2, 24156 + 3]
And
[24156 + 0, 24156 + 1, /* !!!! */ 24156 + 3]
From here it is just a trivial matter of iterating the list and ensuring that the integers occur in sequence without gaps or stalls - that is, each item/integer is exactly one more than the previous.
I could be misinterpreting what you are trying to do, but I think this will work, assuming you don't have to handle ancient dates. See if there are any gaps in the dates converted to "total months"
int totalMonths = date.Year * 12 + (date.Month - 1);
Say I have a list of LockedDate.
A LockedDate has a DateTime and an IsYearly bool. If IsYearly is true then the year should not be considered because it could be any year. Time should never be considered.
Ex: X-Mas, Dec 25 is yearly.
Now I have a List of LockedDate.
There are no duplicates.
Now I need this function:
This function will do:
If a LockedDate is NOT yearly and the day, month, and year are within the range from source, add to return list.
If a LockedDate IS yearly, and its month / day fall in the range, then add a new date for each year in the range.
Say I have Dec 25 with IsYearly as true. My range is Jan 22 2013 to Feb 23 2015 inclusive. would need to add Dec 25 2013 as a new date and Dec 25 2014 as a new Date to the list.
List<Date> GetDateRange(List<LockedDate> source, DateTime start, DateTime end)
{
}
Thanks
Dec 25 Yearly -> Dec 25 2013, Dec 25 2014
Dec 2, 2011 NOT Yearly -> Nothing
March 25, 2013 => March 25 2013
This might give you at least an idea, it's not tested at all yet:
List<DateTime> GetDateRange(List<LockedDate> source, DateTime start, DateTime end)
{
if (start > end)
throw new ArgumentException("Start must be before end");
var ts = end - start;
var dates = Enumerable.Range(0, ts.Days)
.Select(i => start.AddDays(i))
.Where(d => source.Any(ld => ld.Date == d
|| (ld.IsYearly && ld.Date.Month == d.Month && ld.Date.Day == d.Day)));
return dates.ToList();
}
Update Here's the demo with your sample data, it seems to work: http://ideone.com/3KFi97
This code does what you want without using Linq:
List<DateTime> GetDateRange(List<LockedDate> source, DateTime start, DateTime end)
{
List<DateTime> result = new List<DateTime>();
foreach (LockedDate lockedDate in source)
{
if (!lockedDate.IsYearly && (lockedDate.Date >= start && lockedDate.Date <= end))
{
result.Add(lockedDate.Date);
}
else if (lockedDate.IsYearly && (lockedDate.Date >= start && lockedDate.Date <= end))
{
DateTime date = new DateTime(start.Year, lockedDate.Date.Month, lockedDate.Date.Day);
do
{
result.Add(date);
date = date.AddYears(1);
} while (date <= end);
}
}
return result;
}
Make sure to ask about parts you don't understand, so I can explain in detail, but I think it's pretty straightforward.
This code assumes your LockedDate class has a property Date for the DateTime object.
var notYearly = lockDates.Where(d => !d.IsYearly && (d.Date.Date >= start && d.Date.Date <= end)).Select(d => d.Date);
var years = ((end - start).Days / 365) + 2;
var startYear = start.Year;
var yearly = lockDates.Where(d => d.IsYearly)
.SelectMany(d => Enumerable.Range(startYear, years)
.Select(e => new DateTime(e, d.Date.Month, d.Date.Day))
.Where(i => i.Date >= start && i.Date <= end));
var allDates = notYearly.Union(yearly)
Should be more efficient than just iterating through all days between start and end and checking, if that date ok.
If you have a Class LockedDate like this:
public class LockedDate
{
public DateTime Date { get; set; }
public bool IsYearly { get; set; }
...
}
Than you could use this code to get your needed dates:
List<DateTime> GetDateRange(List<LockedDate> source, DateTime start, DateTime end)
{
List<DateTime> dt = new List<DateTime>();
foreach(LockedDate d in source)
{
if(!d.IsYearly)
{
if(start<=d.Date && d.Date<=end)
dt.Add(d.Date);
}
else
{
for(DateTime i = new DateTime(start.Year,d.Date.Month,d.Date.Day);i<=new DateTime(end.Year,d.Date.Month,d.Date.Day);i=i.AddYears(1))
{
dt.Add(i);
}
}
}
return dt;
}