TimeSpan for different years subtracted from a bigger TimeSpan - c#

The language I am using is C#.
I have the folowing dillema.
DateTime A, DateTime B. If A < B then I have to calculate the number of days per year in that timespan and multiply it by a coeficient that corresponds to that year.
My problem is the fact that it can span multiple years.
For example:
Nr of Days in TimeSpan for 2009 * coef for 2009 + Nr of Days in TimeSpan for 2010 * coef for 2010 + etc

You can't do this with a simple TimeSpan, basically. It doesn't know anything about when the span covers - it's just a number of ticks, really.
It sounds to me like there are two cases you need to consider:
A and B are in the same year. This is easy - just subtract one from the other and get the number of days from that
A and B are in different years. There are now either two or three cases:
The number of days after A in A's year. You can work this out by constructing January 1st in the following year, then subtracting A
The number of days in each year completely between A and B (so if A is in 2008 and B is in 2011, this would be 2009 and 2010)
The number of days in B's year. You can work this out by constructing December 31st in the previous year, then subtracting B. (Or possibly January 1st in B's year, depending on whether you want to count the day B is on or not.)
You can use DateTime.IsLeapYear to determine whether any particular year has 365 or 366 days in it. (I assume you're only using a Gregorian calendar, btw. All of this changes if not!)

Here is a little snippet of code that might help
var start = new DateTime(2009, 10, 12);
var end = new DateTime(2014, 12, 25);
var s = from j in Enumerable.Range(start.Year, end.Year + 1 - start.Year)
let _start = start.Year == j ? start : new DateTime(j, 1, 1)
let _end = end.Year == j ? end : new DateTime(j, 12, 31)
select new {
year = j,
days = Convert.ToInt32((_end - _start).TotalDays) + 1
};

If I understood your problem correctly, solving it using Inclusion Exclusion principle would be the best.
Say, if your start date is somewhere in 2008 and the end date is in 2010:
NDAYS(start, end) * coeff_2008
- NDAYS(2009, end) * coeff_2008 + NDAYS(2009, end) * coeff_2009
- NDAYS(2010, end) * coeff_2009 + NDAYS(2010, end) * coeff_2010
Where Ndays computes the number of dates in the interval (TotalDays plus one day).
There is no need to handle leap years specially or compute december 31st.
The details you can work out in a for-loop going over jan first of each year in the span.

Related

How do I move a Period to the other side of a comparison?

When it comes to Periods, - is not supposed to be the inverse of +: adding one month to March 31 produces April 30. Subtracting one month from April 30 produces March 30, not March 31.
I've got a filter on database records that involves periods:
priorDate + period < today
Here, priorDate is derived from a database column, period can be configured by the user, and I need to find all records for which the condition is true.
If the period doesn't involve months or years, I can transform this into
priorDate < today - period
which allows the comparison to be moved from the client side to the database: it allows me to avoid retrieving all records just to discard the ones that don't meet the criteria.
How do I do this if the period does involve months or years?
I can assume the Gregorian calendar, and that that period is non-negative. (Specifically: I can assume priorDate + period >= priorDate for all values of priorDate, but if possible, I'd like to not rule out periods "one month minus one day" just yet.)
If I've got a period of one month, and today is April 30, then I want to figure out that the expression should become priorDate < new LocalDate(2018, 3, 30), which is easy: that's what today - period produces.
If I've got a period of one month, and today is March 30 2018, then today - period will produce February 28, but the expression should instead become a comparison to March 1: if priorDate is exactly new LocalDate(2018, 2, 28), then priorDate + period < new LocalDate(2018, 3, 30) will be true, but priorDate < new LocalDate(2018, 2, 28) will be false.
Given a LocalDate value d and a Period p:
If p only includes months or years:
If naïve addition or subtraction of p would produce an invalid date, the result is reduced to the end of the month. The resulting date will never be "rounded up" to the next month. It will have its year/month components increased exactly by the amount specified in p. Therefore:
d - p will produce the lowest x such that x + p == d, if there is such an x.
In this case, v + p < d is equivalent to v < x.
Otherwise, d - p will produce largest x such that x + p < d.
In this case, v + p < d is equivalent to v <= x, or v < x + Period.FromDays(1).
Which of these two applies can be detected by comparing d - p + p to d.
So priorDate + period < refDate is equivalent to priorDate < F(period, refDate), where F is defined as.
LocalDate F(Period period, LocalDate refDate)
{
var result = refDate - period;
if (result + period != refDate)
result += Period.FromDays(1);
return result;
}
If p includes both days/weeks and months/years:
Adding or subtracting p will add or subtract the month/year components first, and the day/week components next. Moving the period to the other side of the comparison should subtract or add the day/week components first, the month/year components last. The above F doesn't work for e.g. priorDate == new LocalDate(2000, 1, 30), period == Period.FromMonths(1) + Period.FromDays(1), refDate == new LocalDate(2000, 3, 1): here, priorDate + period == refDate (because first a month is added to produce Feb 29, then a day is added to produce Mar 1) but priorDate < F(period, refDate) (because first a month is subtracted to produce Feb 1, then a day is subtracted to produce Jan 31). For that, it's important to subtract the days component first, completely contrary to how Period arithmetic normally works.
So priorDate + period < refDate is equivalent to priorDate < G(period, refDate), where G is defined as.
LocalDate G(Period period, LocalDate refDate)
{
var result =
refDate
- new PeriodBuilder {Weeks = period.Weeks, Days = period.Days}.Build()
- new PeriodBuilder {Years = period.Years, Months = period.Months}.Build();
if (result + period != refDate)
result += Period.FromDays(1);
return result;
}
Note: the subtraction of new PeriodBuilder {Years = period.Years, Months = period.Months}.Build() subtracts years first, months second. This order must not get reversed, unless another fixup is added. A test case that would otherwise fail is d1 == new LocalDate(2000, 2, 29), p == Period.FromYears(1) + Period.FromMonths(1), d2 == new LocalDate(2001, 3, 31). Subtracting a month from d2 produces Feb 28 2001, then subtracting a year would produce Feb 28 2000, and adding a day produces Feb 29 2000. Subtracting a year first from d2 produces Mar 31 2000, then subtracting a month produces Feb 29 2000, and adding a day produces Mar 1 2000, which is the correct result.
It looks like the assumption in my question that period is non-negative is unnecessary.

How to determine fortnight number from date

I have a niggling problem that I am having trouble conceptualising a solution.
I have a payroll system that has the following rules:
The Pay period relating to each pay period is 14 days long starting
on Monday and ending on Sunday
Payday is each fortnight on Thursday following the last day of the pay period.
Pay periods are numbered 1 through to 26 (or 27 in some years) with ONE being the first payday in the financial year and 26 (or 27) being the last in the financial year.
Our financial year runs July 1 to June 30
A known pay fortnight was started on 25/06/2012 and ended on 8/07/2012. The pay day for this period was 12/07/2012. So I do have a few start points.
From any date, I can calculate what its pay date is because I have a known start point. I can also calculate the financial year it relates to easily enough. There are many solutions to those problems on SO.
I now need to allow my users to enter a date and have the application show the pay period it relates to.
What I am struggling with is how can I determine which pay period (1 to 26/27) it is in the financial year? Any ideas?
Now your user enters a date, and you can calculate its pay date, right? Let "payDate" be this pay date, and what you want is "which fortnight of the financial year does payDate fall in, numbered starting with one", right?
Then, you first need to determine the beginning of the financial year:
DateTime yearBegin;
if (payDate.Month >= 7)
yearBegin = new DateTime(payDate.Year, 7, 1);
else
yearBegin = new DateTime(payDate.Year - 1, 7, 1);
With this you can calculate how many days are there between payDate and yearBegin:
int delta = (payDate - yearBegin).Days;
Finally you get the answer:
int period = (delta / 14) + 1;
I understood the first pay day from any given year starts at a different day to always match full periods of 14 days. If that is the case, you could try this code:
static DateTime InitialDate = new DateTime(2012, 06, 25);
private static int PayPeriod(DateTime inputDate)
{
int periodStartDay = (inputDate - InitialDate).Days % 14 + 1;
int periodStartMonth = 07;
int periodStartYear = inputDate.Month >= 7 ? inputDate.Year : inputDate.Year - 1;
DateTime periodStartDate = new DateTime(periodStartYear, periodStartMonth, periodStartDay);
int payPeriod = (inputDate - periodStartDate).Days / 14 + 1;
return payPeriod;
}

Errornous calculation of year/month/day between two dates

What I try to do is calculate the number of years, months and days between two dates.
Unfortunately, there is no method of the .NET Framework which can do this.
What I did is basically the following:
http://www.codeproject.com/Articles/28837/Calculating-Duration-Between-Two-Dates-in-Years-Mo
with a few adaptations from the comments of said website:
var monthDay = new[] { 31, 31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
DayCalculation:
if (fromDate.Day > toDate.Day)
{
increment = monthDay[toDate.Month - 1];
}
if (increment == -1)
{
increment = DateTime.IsLeapYear(toDate.Year) ? 29 : 28;
}
So I have the following effect:
Date1: 1979-01-30
Date2: 2013-03-01
And the output is: 34 Years, 1 Month, -1 Day
The expected output is: 34 Years, 1 Month, 1 Day
This effect happens every time, the Date2 is a date in march.
Do you know what is wrong with this calculation?
Or do you know a better solution how to achieve the desired result?
Thanks in advance
PS: I know that you can calculate the amount of days between two dates, but what I need is the amount of finished years, the amount of finished months, and the amount of finished days
Unfortunately, there is no method of the .NET Framework which can do this.
True, but there is Noda Time instead :) (It's my port of Joda Time to .NET, although there are quite a few differences between the two projects.)
LocalDate start = new LocalDate(1979, 1, 30);
LocalDate end = new LocalDate(2013, 3, 1);
Period period = Period.Between(start, end);
Console.WriteLine("{0} years, {1} months, {2} days",
period.Years, period.Months, period.Days);
Output:
34 years, 1 months, 1 days
Here is a way to calculate the difference without using an external library. Two dates are required. I assume that the first date is no later than the second date. Otherwise you will have to swap them for the calculation to be correct.
var first = new DateTime(1979, 1, 30);
var second = new DateTime(2013, 3, 1);
Here is how to compute the difference. You do not need a table to get the number of days in the month. That information is provided by the DateTime.DaysInMonth function.
var years = second.Year - first.Year;
var months = second.Month - first.Month;
if (months < 0) {
months += 12;
years -= 1;
}
var days = second.Day - first.Day;
if (days < 0) {
var daysInFirstMonth = DateTime.DaysInMonth(first.Year, first.Month);
days += daysInFirstMonth;
months -= 1;
}
Printing the computed values
Console.WriteLine("{0} year(s), {1} month(s), {2} day(s)", years, months, days);
will result in
34 year(s), 1 month(s), 2 day(s)
This produces the same results as the code that you have linked to. However, you expect to get 1 day instead of 2 days. I guess it depends on how you define "days between". If you prefer to count only January 31 as the day between you can subtract 1 from days. Then there will be 0 days between two adjacent days and -1 days "between the same date".

How to convert year, month and day to ticks without using DateTime

long ticks = new DateTime(2012, 1, 31).ToLocalTime().Ticks; // 634635684000000000
But how to do this without DateTime constructor ?
edit
What I actually want is to keep only the years, months and days from the ticks.
long ALL_Ticks = DateTime.Now.Ticks; // 634636033446495283
long Only_YearMonthDay = 634635684000000000; // how to do this ?
I want to use this in a linq-sql query using Linq.Translations.
If you only want the ticks for the date portion of the current datetime you could use:
long Only_YearMonthDay = DateTime.Now.Date.Ticks; //634635648000000000
//DateTime.Now.Date.Ticks + DateTime.Now.TimeOfDay.Ticks == DateTime.Now.Ticks
You could find out how many days are in the calculation and then multiply by 864,000,000,000 (which is how many ticks are in a day). Is that what you are looking for? Bit of documentation here : http://msdn.microsoft.com/en-us/library/system.timespan.ticksperday.aspx.
Happy coding,
Cheers,
Chris.
OK - didn't think this through properly! Ticks represent the amount of 100 nanosecond intervals since 12:00:00 midnight, January 1, 0001. You would need to calculate how many days have passed since that date and then multiply it by the ticks per day value!
If I understand you right, you are not worried about the ticks up to a particular time of the day?! So, it would be something along the lines of :
var ticksToDate = (DateTime.UtcNow - DateTime.MinValue).Days * 864000000000;
Does that answer your question??
That is going to be rather difficult unless you have some other way of getting the current date and time. According to MSDN:
A single tick represents one hundred nanoseconds or one ten-millionth of a second. There are 10,000 ticks in a millisecond.
The value of this property represents the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001, which represents DateTime.MinValue. It does not include the number of ticks that are attributable to leap seconds.
Now, if you know the current date and time, you can calculate how many days have passed since January 1, 0001 and use that to calculate the number of ticks.
I understand you dont want the hour parts of the date. If you use Date, then you only get the day (for example: 01/01/2012 00:00:00)
long ticks = new DateTime(2012, 1, 31).Date.Ticks;
And with any DateTime object already created is the same of course.
long ticks = dateObject.Date.Ticks;
You already have the answer there in your post:
long ALL_Ticks = DateTime.Now.Ticks;
// that's the ticks (from DateTime.MinValue) until 'now' (this very moment)
long ticks = new DateTime(2012, 1, 31).ToLocalTime().Ticks;
// or
long ticks = DateTime.Now.Date.Ticks;
// that's the ticks until the beginning of today
long yearmonthticks = new DateTime(2012, 1, 1).ToLocalTime().Ticks;
// that's the ticks until the beginning of the month
// etc..., the rest is simple subtractions
Since your question doesn't specify any reason not to use the DateTime constructor, this is the best solution for what seems like your problem.
I had a use case where I couldn't use DateTime but needed Years/Months from Ticks.
I used the source behind DateTime to figure out how. To go the other way you can look at the constructor, one of which calls the following code.
private static long DateToTicks(int year, int month, int day) {
if (year >= 1 && year <= 9999 && month >= 1 && month <= 12) {
int[] days = IsLeapYear(year)? DaysToMonth366: DaysToMonth365;
if (day >= 1 && day <= days[month] - days[month - 1]) {
int y = year - 1;
int n = y * 365 + y / 4 - y / 100 + y / 400 + days[month - 1] + day - 1;
return n * TicksPerDay;
}
}
throw new ArgumentOutOfRangeException(null, Environment.GetResourceString("ArgumentOutOfRange_BadYearMonthDay"));
}
This can be found in link below, of course you will need to re-write to suit your needs and look up the constants and IsLeapYear function too.
https://referencesource.microsoft.com/#mscorlib/system/datetime.cs,602

Calculating the elapsed working hours between 2 datetime

Given two datetimes. What is the best way to calculate the number of working hours between them. Considering the working hours are Mon 8 - 5.30, and Tue-Fri 8.30 - 5.30, and that potentially any day could be a public holiday.
This is my effort, seem hideously inefficient but in terms of the number of iterations and that the IsWorkingDay method hits the DB to see if that datetime is a public holiday.
Can anyone suggest any optimizations or alternatives.
public decimal ElapsedWorkingHours(DateTime start, DateTime finish)
{
decimal counter = 0;
while (start.CompareTo(finish) <= 0)
{
if (IsWorkingDay(start) && IsOfficeHours(start))
{
start = start.AddMinutes(1);
counter++;
}
else
{
start = start.AddMinutes(1);
}
}
decimal hours;
if (counter != 0)
{
hours = counter/60;
}
return hours;
}
Before you start optimizing it, ask yourself two questions.
a) Does it work?
b) Is it too slow?
Only if the answer to both question is "yes" are you ready to start optimizing.
Apart from that
you only need to worry about minutes and hours on the start day and end day. Intervening days will obviously be a full 9/9.5 hours, unless they are holidays or weekends
No need to check a weekend day to see if it's a holiday
Here's how I'd do it
// Normalise start and end
while start.day is weekend or holiday, start.day++, start.time = 0.00am
if start.day is monday,
start.time = max(start.time, 8am)
else
start.time = max(start.time, 8.30am)
while end.day is weekend or holiday, end.day--, end.time = 11.59pm
end.time = min(end.time, 5.30pm)
// Now we've normalised, is there any time left?
if start > end
return 0
// Calculate time in first day
timediff = 5.30pm - start.time
day = start.day + 1
// Add time on all intervening days
while(day < end.day)
// returns 9 or 9.30hrs or 0 as appropriate, could be optimised to grab all records
// from the database in 1 or 2 hits, by counting all intervening mondays, and all
// intervening tue-fris (non-holidays)
timediff += duration(day)
// Add time on last day
timediff += end.time - 08.30am
if end.day is Monday then
timediff += end.time - 08.00am
else
timediff += end.time - 08.30am
return timediff
You could do something like
SELECT COUNT(DAY) FROM HOLIDAY WHERE HOLIDAY BETWEEN #Start AND #End GROUP BY DAY
to count the number of holidays falling on Monday, Tuesday, Wednesday, and so forth. Probably a way of getting SQL to count just Mondays and non-Mondays, though can't think of anything at the moment.
especially considering the IsWorkingDay method hits the DB to see if that day is a public holiday
If the problem is the number of queries rather than the amount of data, query the working day data from the data base for the entire day range you need at the beginning instead of querying in each loop iteration.
There's also the recursive solution. Not necessarily efficient, but a lot of fun:
public decimal ElapseddWorkingHours(DateTime start, DateTime finish)
{
if (start.Date == finish.Date)
return (finish - start).TotalHours;
if (IsWorkingDay(start.Date))
return ElapsedWorkingHours(start, new DateTime(start.Year, start.Month, start.Day, 17, 30, 0))
+ ElapsedWorkingHours(start.Date.AddDays(1).AddHours(DateStartTime(start.Date.AddDays(1)), finish);
else
return ElapsedWorkingHours(start.Date.AddDays(1), finish);
}
Take a look at the TimeSpan Class. That will give you the hours between any 2 times.
A single DB call can also get the holidays between your two times; something along the lines of:
SELECT COUNT(*) FROM HOLIDAY WHERE HOLIDAY BETWEEN #Start AND #End
Multiply that count by 8 and subtract it from your total hours.
-Ian
EDIT: In response to below, If you're holiday's are not a constant number of hours. you can keep a HolidayStart and a HolidayEnd Time in your DB and and just return them from the call to the db as well. Do an hour count similar to whatever method you settle on for the main routine.
Building on what #OregonGhost said, rather than using an IsWorkingDay() function at accepts a day and returns a boolean, have a HolidayCount() function that accepts a range and returns an integer giving the number of Holidays in the range. The trick here is if you're dealing with a partial date for your boundry beginning and end days you may still need to determine if those dates are themselves holidays. But even then, you could use the new method to make sure you needed at most three calls the to DB.
Try something along these lines:
TimeSpan = TimeSpan Between Date1 And Date2
cntDays = TimeSpan.Days
cntNumberMondays = Iterate Between Date1 And Date2 Counting Mondays
cntdays = cntdays - cntnumbermondays
NumHolidays = DBCall To Get # Holidays BETWEEN Date1 AND Date2
Cntdays = cntdays - numholidays
numberhours = ((decimal)cntdays * NumberHoursInWorkingDay )+((decimal)cntNumberMondays * NumberHoursInMondayWorkDay )
Use #Ian's query to check between dates to find out which days are not working days. Then do some math to find out if your start time or end time falls on a non-working day and subtract the difference.
So if start is Saturday noon, and end is Monday noon, the query should give you back 2 days, from which you calculate 48 hours (2 x 24). If your query on IsWorkingDay(start) returns false, subtract from 24 the time from start to midnight, which would give you 12 hours, or 36 hours total non-working hours.
Now, if your office hours are the same for every day, you do a similar thing. If your office hours are a bit scattered, you'll have more trouble.
Ideally, make a single query on the database that gives you all of the office hours between the two times (or even dates). Then do the math locally from that set.
The most efficient way to do this is to calculate the total time difference, then subtract the time that is a weekend or holiday. There are quite a few edge cases to consider, but you can simplify that by taking the first and last days of the range and calculating them seperately.
The COUNT(*) method suggested by Ian Jacobs seems like a good way to count the holidays. Whatever you use, it will just handle the whole days, you need to cover the start and end dates separately.
Counting the weekend days is easy; if you have a function Weekday(date) that returns 0 for Monday through 6 for Sunday, it looks like this:
saturdays = ((finish - start) + Weekday(start) + 2) / 7;
sundays = ((finish - start) + Weekday(start) + 1) / 7;
Note: (finish - start) isn't to be taken literally, replace it with something that calculates the time span in days.
Dim totalMinutes As Integer = 0
For minute As Integer = 0 To DateDiff(DateInterval.Minute, contextInParameter1, contextInParameter2)
Dim d As Date = contextInParameter1.AddMinutes(minute)
If d.DayOfWeek <= DayOfWeek.Friday AndAlso _
d.DayOfWeek >= DayOfWeek.Monday AndAlso _
d.Hour >= 8 AndAlso _
d.Hour <= 17 Then
totalMinutes += 1
Else
Dim test = ""
End If
Next minute
Dim totalHours = totalMinutes / 60
Piece of Cake!
Cheers!

Categories