Reputation: 1402
For an application I need to do some calculations with dates. I found how I can get the difference in months between 2 dates but not how I can add these months to another date.
Function to calculate difference in months:
public static decimal GetMonthsInRange(DateTime start, DateTime finish)
{
var monthsApart =
Math.Abs(12 * (start.Year - finish.Year) + start.Month - finish.Month) - 1;
decimal daysInStartMonth = DateTime.DaysInMonth(start.Year, start.Month);
decimal daysInFinishMonth = DateTime.DaysInMonth(finish.Year, finish.Month);
var daysApartInStartMonth = (daysInStartMonth - start.Day + 1) / daysInStartMonth;
var daysApartInFinishMonth = finish.Day / daysInFinishMonth;
return monthsApart + daysApartInStartMonth + daysApartInFinishMonth;
}
Example:
The difference in months I get is 4.5 months and I need to add that to another date. But the DateTime.AddMonths
function only accepts integer numbers.
How can I do this?
Grts, Nanou
Upvotes: 1
Views: 1401
Reputation:
Your GetMonthsInRange
works like this: each day is counted with a weight inversely proportional to the number of days in the month. So the same time difference of one day (i.e. 24 hours) - if it falls in a different month - corresponds to different decimal results, according to your strange rule of GetMonthsInRange
.
I'm saying that it is strange, but maybe one could have some contractual/accounting reasons to redefine the difference in months. Like for example if one want to compute a time-equivalent period based on a monthly amount...
If you want to have the reverse engineered AddMonths
, based on the same rule, so to be consistent with that position, you could write an extension like the following
public static class Extensions
{
public static DateTime AddMonths(this DateTime date, decimal months)
{
var start = date;
decimal daysInStartMonth = DateTime.DaysInMonth(start.Year, start.Month);
var daysApartInStartMonth = (daysInStartMonth - start.Day + 1) / daysInStartMonth;
if (months <= daysApartInStartMonth)
{
return date.AddDays((double)(months * daysInStartMonth - 1));
}
var finish = date.AddMonths(1);
int monthsApart = 0;
if (months > daysApartInStartMonth + 1)
{
monthsApart = (int)(months - daysApartInStartMonth);
finish = finish.AddMonths(monthsApart);
}
decimal daysInFinishMonth = DateTime.DaysInMonth(finish.Year, finish.Month);
var startOfFinishMonth = new DateTime(finish.Year, finish.Month, 1).AddDays(-1);
decimal remaining = months - daysApartInStartMonth - monthsApart;
return startOfFinishMonth.AddDays((double)(remaining * daysInFinishMonth));
}
}
Test if this fits your needs. I assume you want GetMonthsInRange
and AddMonths
to be consistent.
A possible test case generator
bool ok = true;
for (int i = 0; i < 1000; i++)
{
var day1 = new DateTime(2017, 2, 16);
var day2 = new DateTime(2017, 2, 16).AddDays(i);
var test = Extensions.GetMonthsInRange(day1, day2);
var res = day1.AddMonths(test);
var check = DateTime.Compare(day2, res);
if (check != 0)
{
ok = false;
break;
}
}
Upvotes: 0
Reputation: 186843
Technically, you can try using linear interpolation between two AddMonth
results:
private static DateTime AddMonths(DateTime source, double months) {
int left = (int) Math.Floor(months);
int right = (int) Math.Ceiling(months);
double days =
(source.AddMonths(right) - source.AddMonths(left)).TotalDays *
(months - Math.Floor(months));
return source.AddMonths(left).AddDays(days);
}
Tests:
DateTime source = DateTime.Today;
double add = 1.0;
Console.Write($"{source} + {add} = {AddMonths(source, add)}");
add = 1.1;
Console.Write($"{source} + {add} = {AddMonths(source, add)}");
add = 1.5;
Console.Write($"{source} + {add} = {AddMonths(source, add)}");
add = 1.95;
Console.Write($"{source} + {add} = {AddMonths(source, add)}");
add = 2.0;
Console.Write($"{source} + {add} = {AddMonths(source, add)}");
Outcome:
16.02.2017 0:00:00 + 1 = 16.03.2017 0:00:00
16.02.2017 0:00:00 + 1.1 = 19.03.2017 2:24:00
16.02.2017 0:00:00 + 1.5 = 31.03.2017 12:00:00
16.02.2017 0:00:00 + 1.95 = 14.04.2017 10:48:00
16.02.2017 0:00:00 + 2 = 16.04.2017 0:00:00
However, remember, months have different lengths [28..31]
days, and interpolation is just estimation.
Upvotes: 0
Reputation: 26896
Obviously, you should not calculate difference in months if you want to add this difference to some DateTime
since this difference can be even more confusing, say 4.33 months or something like this.
Instead - just calculate difference in days (in your concrete example it will be (finish - start).TotalDays
) and use AddDays
.
Upvotes: 4
Reputation: 1099
The DateTime has "AddMonths" and "AddDays" functions among others. These take integer values. If you have fractional months, you should preserved the integer difference in days that you worked out and add that value as days for the required accuracy. https://msdn.microsoft.com/en-us/library/system.datetime(v=vs.110).aspx
Also, you do have a problem because you're doing ABS calculations for months apart (so that it works both ways), but you don't seen to be making the same before/after corrections for the days in the month calcuations.
Upvotes: 0