I'm trying to workout the amount of time between two LocalDateTime values and exclude specific dates (in this example, it's bank holidays).
var bankHolidays = new[] { new LocalDate(2013, 12, 25), new LocalDate(2013, 12, 26) };
var localDateTime1 = new LocalDateTime(2013, 11, 18, 10, 30);
var localDateTime2 = new LocalDateTime(2013, 12, 29, 10, 15);
var differenceBetween = Period.Between(localDateTime1, localDateTime2, PeriodUnits.Days | PeriodUnits.HourMinuteSecond);
The differenceBetween value shows the number of days/hours/minutes/seconds between the two dates, as you would expect.
I could check every single day from the start date and see if the bankHolidays collection contains that date e.g.
var bankHolidays = new[] { new LocalDate(2013, 12, 25), new LocalDate(2013, 12, 26) };
var localDateTime1 = new LocalDateTime(2013, 11, 18, 10, 30);
var localDateTime2 = new LocalDateTime(2013, 12, 29, 10, 15);
var differenceBetween = Period.Between(localDateTime1, localDateTime2, PeriodUnits.Days | PeriodUnits.HourMinuteSecond);
var london = DateTimeZoneProviders.Tzdb["Europe/London"];
for (var i = 1; i < differenceBetween.Days; ++i)
{
var x = localDateTime1.InZoneStrictly(london) + Duration.FromStandardDays(i);
if (bankHolidays.Any(date => date == x.Date))
{
//subtract one day for the period.
}
}
I feel like I'm missing some obvious and there should be an easier method, is there a simpler way to find a period between two dates whilst excluding certain dates?
I also need to include weekends in this exclusion too, the obvious way seems to be to check the day of the week for weekends whilst checking bank holidays, this just doesn't seem like the best/correct way of handling it though.
I feel like I'm missing some obvious and there should be an easier method, is there a simpler way to find a period between two dates whilst excluding certain dates?
Well, it's relatively easy to count the number of bank holidays included in a date-to-date range:
Sort all the bank holidays in chronological order
Use a binary search to find out where the start date would come in the collection
Use a binary search to find out where the end date would come in the collection
Subtract one index from another to find how many entries are within that range
Work out the whole period using Period.Between as you're already doing
Subtract the number of entries in the range from the total number of days in the range
The fiddly bit is taking into account that the start and/or end dates may be bank holidays. There's a lot of potential for off-by-one errors, but with a good set of unit tests it should be okay.
Alternatively, if you've got relatively few bank holidays, you can just use:
var period = Period.Between(start, end,
PeriodUnits.Days | PeriodUnits.HourMinuteSecond);
var holidayCount = holidays.Count(x => x >= start && x <= end);
period = period - Period.FromDays(holidayCount);
Just use TimeSpan to get the difference, all times are in your current local time zone:
var bankHolidays = new[] { new DateTime(2013, 12, 25), new DateTime(2013, 12, 26) };
var localDateTime1 = new DateTime(2013, 11, 18, 10, 30, 0);
var localDateTime2 = new DateTime(2013, 12, 29, 10, 15, 0);
var span = localDateTime2 - localDateTime1;
var holidays = bankHolidays[1] - bankHolidays[0];
var duration = span-holidays;
Now duration is your time elapsed between localDateTime1 and localDateTime2.
If you want to exlude two dates via the bankHolidays you can easiely modify the operations above.
You might use an extra method for this operation:
public static TimeSpan GetPeriod(DateTime start, DateTime end, params DateTime[] exclude)
{
var span = end - start;
if (exclude == null) return span;
span = exclude.Where(d => d >= start && d <= end)
.Aggregate(span, (current, date) => current.Subtract(new TimeSpan(1, 0, 0, 0)));
return span;
}
Now you can just use this:
var duration = GetPeriod(localDateTime1, localDateTime2, bankHolidays);
Related
I have a list of dates:
var dates = new List<DateTime>
{
new DateTime(2016, 01, 01),
new DateTime(2016, 02, 01),
new DateTime(2016, 03, 01),
new DateTime(2016, 04, 01),
new DateTime(2016, 05, 01)
};
Now given a certain date, a "StartDate". What is the easiest way to create a list of dates after the startdate, and the last date before?
I.E. - If I supply the date DateTime(2016, 03, 15), I need to return
DateTime(2016, 03, 01),
DateTime(2016, 04, 01),
DateTime(2016, 05, 01)
It could be as simple as finding the last "Active" date and then just using the where from that date. But I'm unsure on how to do this without making it really complicated.
If your list is already sorted, you can use a binary search:
var index = dates.BinarySearch(start);
// If the precise value isn't found, index will be the bitwise complement
// of the first index *later* than the target, so we need to subtract 1.
// But if there were no values earlier, we should start from 0.
if (index < 0)
{
index = Math.Max(~index - 1, 0);
}
return dates.Skip(index).ToList();
This assumes the dates are unique. If there are multiple dates the same as start, there's no guarantee that it will find the first one. If that's a concern, you'd need to search backwards until you found the first match.
You haven't specified whether if there's an exact match, you want to include the date before that or not. If you do, you'll need to adjust this code a bit.
Without making it complicated and if i understand your requirements correctly. You want all the dates after the StartDate and the last entry before the first matching (If Any). Then I find this the easiest most readable way of doing it:
var results = dates.FindAll(x => x >= StartDate);
int index = dates.FindLastIndex(x => x < StartDate);
// there might be no match, if all the list is resulted
if (index >= 0)
results.Insert(0, dates[index]);
If you prefer one query style, you can do the below (I find it not readable):
var results = dates.Where(x => x >= StartDate)
.Concat(dates.Where(x => x < StartDate)
.OrderByDescending(x => x).Take(1));
Last Alternative if you like fancy ways:
int startIndex = dates.FindLastIndex(x=> x < StartDate);
startIndex = Math.Max(0, startIndex);
var results = dates.Skip(startIndex).ToList();
var partialResult = dates.Where(x => x >= date).ToList();
partialResult.Add(dates.Where(x => x < date).Max());
IList<DateTime> result = partialResult.OrderBy(x => x).ToList();
I have a trouble with NodaTime lib. My goal: compute Year/Month/Date between two dates. So, here is my test example:
private static void Main()
{
var list = new List<Tuple<DateTime, DateTime>>
{
new Tuple<DateTime, DateTime>(new DateTime(1980, 1, 1), new DateTime(1983, 12, 31)),
new Tuple<DateTime, DateTime>(new DateTime(2009, 1, 1), new DateTime(2015, 01, 23))
};
var totalPeriod = Period.Zero;
foreach (var tuple in list)
{
var dateFrom = tuple.Item1;
var dateTo = tuple.Item2;
var ld1 = new LocalDate(dateFrom.Year, dateFrom.Month, dateFrom.Day);
var ld2 = new LocalDate(dateTo.Year, dateTo.Month, dateTo.Day);
var period = Period.Between(ld1, ld2, PeriodUnits.YearMonthDay);
totalPeriod += period;
}
Console.WriteLine("Years: {0}, Months: {1}, Days: {2}",
totalPeriod.Years,
totalPeriod.Months,
totalPeriod.Days);
Console.Read();
}
The output is:
Years: 9, Months: 11, Days: 52
It's wrong for me. I want to get, for example, the next output (Of course, the output depends on number of days in month, assuming that there are 31 days in our month):
Years: 10, Months: 0, Days: 21
So, I want that days was rounded to years and month. How I can get this?
The answer:
Using Matt's answer I created the next solution:
foreach (var tuple in list)
{
var dateFrom = tuple.Item1;
var dateTo = tuple.Item2;
var period = Period.Between(LocalDateTime.FromDateTime(dateFrom).Date, LocalDateTime.FromDateTime(dateTo).Date, PeriodUnits.YearMonthDay);
totalPeriod += period;
}
// trying clarify the period
while(totalPeriod.Days >= 30)
{
totalPeriod = totalPeriod - Period.FromDays(30);
totalPeriod = totalPeriod + Period.FromMonths(1);
while (totalPeriod.Months >= 12)
{
totalPeriod = totalPeriod - Period.FromMonths(12);
totalPeriod = totalPeriod + Period.FromYears(1);
}
}
Richard was right in his comment on the OP. The problem is that the months and years aren't distinct quantities unto themselves. One must have a frame of reference to "count" them. You have that reference when you do the Period.Between operation, but it's lost by the time you try to add the periods together.
If you check the periods that are being added, it makes sense:
First: Years: 3, Months: 11, Days: 30
Second: Years: 6, Months: 0, Days: 22
Total: Years: 9, Months: 11, Days: 52
In order to round as you would like, the 22 days being added to the 30 days would somehow have to know which month was being referenced. Even if you retained the original information - which one would you use? You could well have a 28-day month on one side, and a 31-day month on the other.
The best you could do would be to artificially round the results yourself afterwards, and choose a flat value (such as 30 days) to represent all months.
Oh, one minor thing (unrelated to your question) - To go from a DateTime to a LocalDate, try LocalDateTime.FromDateTime(dt).Date. :)
Assume I have a user interface where the user can select days. Is there a way to check if the days selected are sequential, such as:
4/4, 4/5, 4/6, 4/7, 4/8, 4/9, 4/10 or
4/29, 4/30, 5/1, 5/2, 5/3
I know I probably can loop through the date range and check, but I was more curious if there was a built in method already to check for this.
Regarding the above scenarios, they are in order and they can roll over into the next month.
I am using the .NET Framework 2.0 and can't use LINQ.
Regarding Tom's answer:
DateTime dtStart = new DateTime(2011,5,4);
DateTime dtEnd = new DateTime(2011,5,11);
int numberOfDaysSelected = 7; //Assume 7 days were selected.
TimeSpan ts = dtEnd - dtStart;
if(ts.Days == numberOfDaysSelected - 1)
{
Console.WriteLine("Sequential");
}
else
{
Console.WriteLine("Non-Sequential");
}
I do not believe there is a built in method to achieve your desired results but if you can easily tell the earliest and latest dates, you could create a new TimeSpan by subtracting the the earliest date from the latest date and then verifying that the number of days of the timespan matches the number of dates selected - 1.
You didn't tell us if the days are ordered.
You didn't tell us if they might fall over a month boundary as in
30, 31, 1.
I'll assume ordered, and I'll assume they won't fall over a month boundary (because your example is ordered, and it doesn't fall over a month boundary).
Then you can say
public bool IsSequential(this IEnumerable<DateTime> sequence) {
Contract.Requires(sequence != null);
var e = sequence.GetEnumerator();
if(!e.MoveNext()) {
// empty sequence is sequential
return true;
}
int previous = e.Current.Date;
while(e.MoveNext()) {
if(e.Current.Date != previous.AddDays(1)) {
return false;
}
previous = e.Current.Date;
}
return true;
}
Note that this solution requires only walking the sequence once. If you don't have an ordered sequence, or if you permit falling over a month boundary the solution is more complicated.
Nothing built in but you can build one easily using Linq:
List<DateTime> timeList = new List<DateTime>();
//populate list..
bool isSequential = timeList.Zip(timeList.Skip(1),
(a, b) => b.Date == a.Date.AddDays(1))
.All(x => x);
Edited - misunderstood question first to mean ascending in time as opposed to sequential - fixed that.
Extension method using Linq:
public static bool IsContiguous(this IEnumerable<DateTime> dates)
{
var startDate = dates.FirstOrDefault();
if (startDate == null)
return true;
//.All() doesn't provide an indexed overload :(
return dates
.Select((d, i) => new { Date = d, Index = i })
.All(d => (d.Date - startDate).Days == d.Index);
}
Testing it:
List<DateTime> contiguousDates = new List<DateTime>
{
new DateTime(2011, 05, 05),
new DateTime(2011, 05, 06),
new DateTime(2011, 05, 07),
};
List<DateTime> randomDates = new List<DateTime>
{
new DateTime(2011, 05, 05),
new DateTime(2011, 05, 07),
new DateTime(2011, 05, 08),
};
Console.WriteLine(contiguousDates.IsContiguous());
Console.WriteLine(randomDates.IsContiguous());
Returns
True
False
EDIT:
.NET 2-like answer:
public static bool CheckContiguousDates(DateTime[] dates)
{
//assuming not null and count > 0
var startDate = dates[0];
for (int i = 0; i < dates.Length; i++)
{
if ((dates[i] - startDate).Days != i)
return false;
}
return true;
}
You can use the TimeGapCalculator of the Time Period Library for .NET to find gaps between multiple time periods (independent of order, count and overlapping):
// ----------------------------------------------------------------------
public void SequentialPeriodsDemo()
{
// sequential
ITimePeriodCollection periods = new TimePeriodCollection();
periods.Add( new Days( new DateTime( 2011, 5, 4 ), 2 ) );
periods.Add( new Days( new DateTime( 2011, 5, 6 ), 3 ) );
Console.WriteLine( "Sequential: " + IsSequential( periods ) );
periods.Add( new Days( new DateTime( 2011, 5, 10 ), 1 ) );
Console.WriteLine( "Sequential: " + IsSequential( periods ) );
} // SequentialPeriodsDemo
// --------------------------------------------------------------------
public bool IsSequential( ITimePeriodCollection periods, ITimePeriod limits = null )
{
return new TimeGapCalculator<TimeRange>(
new TimeCalendar() ).GetGaps( periods, limits ).Count == 0;
} // IsSequential
When given a start date a need to do various calculations on it to produce 3 other dates.
Basically I need to work out what date the user has been billed up to for different frequencies based on the current date.
Bi-Annually (billed twice a year),
Quarterly (billed 4 times a year),
and Two Monthly (billed ever other month).
Take the date 26/04/2008
- BiAnnually: This date would have been last billed on 26/10/2010 and should give the date 26/04/2011.
- Quarterly: This date would have been last billed on 26/01/2011 and should give the date 26/04/2011.
- Two Month: This date would have been last billed on 26/12/2010 and should give the date 26/02/2011.
Assistance is much appreciated.
I think that you can just do like this:
public void FindNextDate(DateTime startDate, int interval);
DateTime today = DateTime.Today;
do {
startDate = startDate.AddMonths(interval);
} while (startDate <= today);
return startDate;
}
Usage:
DateTime startDate = new DateTime(2008, m4, 26);
DateTime bi = FindNextDate(startDate, 6);
DateTime quarterly = FindNextDate(startDate, 3);
DateTime two = FindNextDate(startDate, 2);
I think all you want is something like
DateTime x = YourDateBasis;
y = x.AddMonths(6);
y = x.AddMonths(3);
y = x.AddMonths(2);
Then to edit from comment,
Date Math per the period cycle of the person's account, you would simply need the start and end date and keep adding respective months until you've created all expected months. Almost like that of a loan payment that's due every month for 3 years
DateTime CurrentDate = DateTime.Now;
while( CurrentDate < YourFinalDateInFuture )
{
CurrentDate = CurrentDate.AddMonths( CycleFrequency );
Add Record into your table as needed
Perform other calcs as needed
}
enum BillPeriod
{
TwoMonth = 2,
Quarterly = 3,
SemiAnnually = 6,
BiAnnually = 24
}
public Pair<Datetime, Datetime> BillDates(Datetime currentBillDate, BillPeriod period)
{
Datetime LastBill = currentBillDate.AddMonths(-1 * (int)period);
Datetime NextBill = currentBillDate.AddMonths((int)period);
return new Pair<Datetime,Datetime>(LastBill, NextBill);
}
This is a terrible solution, but it works. Remember, red-light, green-light, refactor. Here, we're at green-light:
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
Console.WriteLine(GetLastBilled(new DateTime(2008, 4, 26), 6));
Console.WriteLine(GetNextBilled(new DateTime(2008, 4, 26), 6));
Console.WriteLine(GetLastBilled(new DateTime(2008, 4, 26), 4));
Console.WriteLine(GetNextBilled(new DateTime(2008, 4, 26), 4));
Console.WriteLine(GetLastBilled(new DateTime(2008, 4, 26), 2));
Console.WriteLine(GetNextBilled(new DateTime(2008, 4, 26), 2));
Console.WriteLine("Complete...");
Console.ReadKey(true);
}
static DateTime GetLastBilled(DateTime initialDate, int billingInterval) {
// strip time and handle staggered month-end and 2/29
var result = initialDate.Date.AddYears(DateTime.Now.Year - initialDate.Year);
while (result > DateTime.Now.Date) {
result = result.AddMonths(billingInterval * -1);
}
return result;
}
static DateTime GetNextBilled(DateTime initialDate, int billingInterval) {
// strip time and handle staggered month-end and 2/29
var result = initialDate.Date.AddYears(DateTime.Now.Year - initialDate.Year);
while (result > DateTime.Now.Date) {
result = result.AddMonths(billingInterval * -1);
}
result = result.AddMonths(billingInterval);
return result;
}
}
}
This is really tricky. For example, you need to take into account that the date you billed could have been 2/29 on a leap year, and not all months have the same number of days. That's why I did the initialDate.Date.AddYears(DateTime.Now.Year - initialDate.Year); call.
How can I calculate/find the week-number of a given date?
var currentCulture = CultureInfo.CurrentCulture;
var weekNo = currentCulture.Calendar.GetWeekOfYear(
new DateTime(2013, 12, 31),
currentCulture.DateTimeFormat.CalendarWeekRule,
currentCulture.DateTimeFormat.FirstDayOfWeek);
Be aware that this is not ISO 8601 compatible. In Sweden we use ISO 8601 week numbers but even though the culture is set to "sv-SE", CalendarWeekRule is FirstFourDayWeek, and FirstDayOfWeek is Monday the weekNo variable will be set to 53 instead of the correct 1 in the above code.
I have only tried this with Swedish settings but I'm pretty sure that all countries (Austria, Germany, Switzerland and more) using ISO 8601 week numbers will be affected by this problem.
Peter van Ooijen and Shawn Steele has different solutions to this problem.
Here's a compact solution
private static int WeekOfYearISO8601(DateTime date)
{
var day = (int)CultureInfo.CurrentCulture.Calendar.GetDayOfWeek(date);
return CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(date.AddDays(4 - (day == 0 ? 7 : day)), CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
}
It's been tested for the following dates
var datesAndISO8601Weeks = new Dictionary<DateTime, int>
{
{new DateTime(2000, 12, 31), 52},
{new DateTime(2001, 1, 1), 1},
{new DateTime(2005, 1, 1), 53},
{new DateTime(2007, 12, 31), 1},
{new DateTime(2008, 12, 29), 1},
{new DateTime(2010, 1, 3), 53},
{new DateTime(2011, 12, 31), 52},
{new DateTime(2012, 1, 1), 52},
{new DateTime(2013, 1, 2), 1},
{new DateTime(2013, 12, 31), 1},
};
foreach (var dateWeek in datesAndISO8601Weeks)
{
Debug.Assert(WeekOfYearISO8601(dateWeek.Key) == dateWeek.Value, dateWeek.Key.ToShortDateString() + " should be week number " + dateWeek.Value + " but was " + WeekOfYearISO8601(dateWeek.Key));
}
public static int GetWeekNumber(DateTime dtPassed)
{
CultureInfo ciCurr = CultureInfo.CurrentCulture;
int weekNum = ciCurr.Calendar.GetWeekOfYear(dtPassed, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
return weekNum;
}
Check out GetWeekOfYear on MSDN has this example:
using System;
using System.Globalization;
public class SamplesCalendar {
public static void Main() {
// Gets the Calendar instance associated with a CultureInfo.
CultureInfo myCI = new CultureInfo("en-US");
Calendar myCal = myCI.Calendar;
// Gets the DTFI properties required by GetWeekOfYear.
CalendarWeekRule myCWR = myCI.DateTimeFormat.CalendarWeekRule;
DayOfWeek myFirstDOW = myCI.DateTimeFormat.FirstDayOfWeek;
// Displays the number of the current week relative to the beginning of the year.
Console.WriteLine( "The CalendarWeekRule used for the en-US culture is {0}.", myCWR );
Console.WriteLine( "The FirstDayOfWeek used for the en-US culture is {0}.", myFirstDOW );
Console.WriteLine( "Therefore, the current week is Week {0} of the current year.", myCal.GetWeekOfYear( DateTime.Now, myCWR, myFirstDOW ));
// Displays the total number of weeks in the current year.
DateTime LastDay = new System.DateTime( DateTime.Now.Year, 12, 31 );
Console.WriteLine( "There are {0} weeks in the current year ({1}).", myCal.GetWeekOfYear( LastDay, myCWR, myFirstDOW ), LastDay.Year );
}
}
My.Computer.Info.InstalledUICulture.DateTimeFormat.Calendar.GetWeekOfYear(yourDateHere, CalendarWeekRule.FirstDay, My.Computer.Info.InstalledUICulture.DateTimeFormat.FirstDayOfWeek)
Something like this...
I know this is late, but since it came up in my search I thought I would throw a different solution in. This was a c# solution.
(int)(Math.Ceiling((decimal)startDate.Day / 7)) + (((new DateTime(startDate.Year, startDate.Month, 1).DayOfWeek) > startDate.DayOfWeek) ? 1 : 0);
If you want the ISO 8601 Week Number, in which weeks start with a monday, all weeks are seven days, and week 1 is the week containing the first thursday of the year, this may be a solution.
Since there doesn't seem to be a .Net-culture that yields the correct ISO-8601 week number, I'd rater bypass the built in week determination alltogether, and do the calculation manually, instead of atempting to correct a partially correct result.
What I ended up with is the following extension method:
public static int GetIso8601WeekNumber(this DateTime date)
{ var thursday = date.AddDays(3 - ((int)date.DayOfWeek + 6) % 7);
return 1 + (thursday.DayOfYear - 1) / 7;
}
First of all, ((int)date.DayOfWeek + 6) % 7) determines the weekday number, 0=monday, 6=sunday.
date.AddDays(-((int)date.DayOfWeek + 6) % 7) determines the date of the monday preceiding the requested week number.
Three days later is the target thursday, which determines what year the week is in.
If you divide the (zero based) day-number within the year by seven (round down), you get the (zero based) week number in the year.
In c#, integer calculation results are round down implicitly.
var cultureInfo = CultureInfo.CurrentCulture;
var calendar = cultureInfo.Calendar;
var calendarWeekRule = cultureInfo.DateTimeFormat.CalendarWeekRule;
var firstDayOfWeek = cultureInfo.DateTimeFormat.FirstDayOfWeek;
var lastDayOfWeek = cultureInfo.LCID == 1033 //En-us
? DayOfWeek.Saturday
: DayOfWeek.Sunday;
var lastDayOfYear = new DateTime(date.Year, 12, 31);
//Check if this is the last week in the year and it doesn`t occupy the whole week
var weekNumber = calendar.GetWeekOfYear(date, calendarWeekRule, firstDayOfWeek);
return weekNumber == 53 && lastDayOfYear.DayOfWeek != lastDayOfWeek
? 1
: weekNumber;
It works well both for US and Russian cultures. ISO 8601 also will be correct, `cause Russian week starts at Monday.