Behavior of DateTime.AddYears on leap year - c#

Can anyone explain the mathematical or simply the reasoning behind the leap year calculations in .NET when using AddYears method on DateTime?
If you take the 29th Feb 2012 and add a year, you get the 28th Feb 2013, not the 1st Mar 2013 (day before one year later).
If you add one year to 31st Jan 2012, you get 31st Jan 2013 (same date one year later).
I think most people would assume that "one year from 29.02.leapX is 01.03.leapX+1".
Example:
// Testing with 29th Feb
var now1 = DateTime.Parse("2012-02-29 15:00:00");
var results1 = new DateTime[]
{
now1.AddYears(1),
now1.AddYears(2),
now1.AddYears(3),
now1.AddYears(4)
};
foreach(var dt in results1)
{
Console.WriteLine(dt.ToString("s"));
}
// Output:
// 2013-02-28T15:00:00
// 2014-02-28T15:00:00
// 2015-02-28T15:00:00
// 2016-02-29T15:00:00
// Testing with 31st Jan
var now2 = DateTime.Parse("2012-01-31 13:00:00");
var results2 = new DateTime[]
{
now2.AddYears(1),
now2.AddYears(2),
now2.AddYears(3),
now2.AddYears(4)
};
foreach(var dt in results2)
{
Console.WriteLine(dt.ToString("s"));
}
// Output:
// 2013-01-31T13:00:00
// 2014-01-31T13:00:00
// 2015-01-31T13:00:00
// 2016-01-31T13:00:00

I think most people would assume that "one year from 29.02.leapX is 01.03.leapX+1".
I wouldn't. I would normally expect truncation. It's fundamentally similar to adding one month to January 30th - I'd expect to get the last day in February. In both cases, you're adding a "larger unit" (month or year) and a "smaller unit" (day) is being truncated to fit in with the year/month combination.
(This is how Joda Time and Noda Time behave too, btw.)
As Tim mentioned in comments, it's documented that way too:
The AddYears method calculates the resulting year taking into account leap years. The month and time-of-day part of the resulting DateTime object remains the same as this instance.
So the month has to stay as February; the year will change based on how many years are being added, obviously - so the day has to adjust to stay valid.

With your rationale then 1-Mar-2012 would become 2-Mar-2012 when you added a year. If you add this shift for all prior leap years then you are going to find your calculation massively adrift. The only sensible response is to return 28-Feb for non-leap years.

It is interesting, nether-the-less ..
e.g. this function is sometimes used:
private static int Age(DateTime birthDate, DateTime asAtDate)
{
// Calculate age in years
int age = asAtDate.Year - birthDate.Year;
if (asAtDate < birthDate.AddYears(age)) age--;
if (age < 0) age = 0;
return age;
}
If a person was born on 29Feb2016, this function is going to conclude they have reached age 1 on 28Feb2017.
I noted Excel Function examples as per:
=DATEDIF(DATE(2016,2,28),DATE(2017,2,28),"Y")
gives result of 1
=DATEDIF(DATE(2016,2,29),DATE(2017,2,28),"Y")
gives result of 0
=DATEDIF(DATE(2016,2,29),DATE(2017,3,1),"Y")
gives result of 1
=DATEDIF(DATE(2016,3,1),DATE(2017,3,1),"Y")
gives result of 1

Related

Leap Year Messing up Future Date Calculations

So My coworker and I have been in the works on creating a Blazor app for an expiration date calculator for the labs at our company. We are basically almost done until we realized that leap years exist and are messing up the calculation.
We know how to check for a leap year but the thing is, once we account for a leap year (e.g 2024) every calculation for every year after that turns up wrong (e.g 2025,2026, etc.).
We don't know how to remedy this beyond just creating a check if it's a leap year or not since the calculation is just messed up every year after that... thoughts or has anyone else run into this issue?
The logic part of the code is listed below:
#code {
private RecordEditContext recordEditContext = new RecordEditContext(new());
private ExpirationDate ExpiryDate = new ExpirationDate();
private string errorMessage = string.Empty;
private bool IsError => this.errorMessage != string.Empty;
private void CalculateExpiryDate()
{
this.errorMessage = string.Empty;
this.ExpiryDate.Value = DateTime.MinValue;
this.recordEditContext.SetToClean();
if ((recordEditContext.IsBeforeDate == false) && (recordEditContext.IsPlate == false) && (recordEditContext.ShelfLife >= 90))
{
this.ExpiryDate.Value = (recordEditContext.MixDate + TimeSpan.FromDays(recordEditContext.ShelfLife)) - TimeSpan.FromDays(30);
this.ExpiryDate.Format = ExpirationDate.ExpiryDateFormat.MonthYear;
DateTime expDay = this.ExpiryDate.Value;
DateTime endOfMonth = new DateTime(expDay.Year, expDay.Month, DateTime.DaysInMonth(expDay.Year, expDay.Month));
return;
}
if ((recordEditContext.IsBeforeDate == true) || (recordEditContext.IsPlate == true) || (recordEditContext.ShelfLife < 90))
{
this.ExpiryDate.Value = (recordEditContext.MixDate + TimeSpan.FromDays(recordEditContext.ShelfLife)).AddDays(-1);
this.ExpiryDate.Format = ExpirationDate.ExpiryDateFormat.YearMonthDay;
return;
}
this.errorMessage = "Please Try Again. Information Not Satisfactory.";
}
let me know if you guys need more information!
It sounds to me like the business wants a product that expires to show the expiration date as 3 years as shown on a calendar minus 30 days. At least from what I can tell. So specifically you want November 1, 2022 with a 3 year expiration date to be November 1, 2025 minus 30 days. To do this, you cannot count by days alone as you are.
A reproduction of what you're doing now:
//this is what you're currently doing
//(recordEditContext.MixDate + TimeSpan.FromDays(recordEditContext.ShelfLife)) - TimeSpan.FromDays(30)
DateTime MixDate = new DateTime(2022, 12, 01);
TimeSpan ShelfLife = TimeSpan.FromDays(1095);
TimeSpan ThirtyDays = TimeSpan.FromDays(30);
var TimeAsIs = (MixDate + ShelfLife) - ThirtyDays;
Console.WriteLine($"### original ###\nMixed: {MixDate} Expires: {TimeAsIs}");
How you might fix most of the issue:
//This sets the day of expiration based on the mix date and then calculates the date you need.
//If you need it to be November 1, 2025 minus 30 days
//because it was mixed on November 1 2022, this is how.
DateTime ExpirationDateIsThreeYears = new DateTime(MixDate.Year + 3, MixDate.Month, MixDate.Day);
var NewTime = ExpirationDateIsThreeYears - ThirtyDays;
Console.WriteLine($"### new ###\nMixed: {MixDate} Expires: {NewTime}");
Console.ReadLine();
This outputs:
### original ###
Mixed: 12/1/2022 12:00:00 AM Expires: 10/31/2025 12:00:00 AM
### new ###
Mixed: 12/1/2022 12:00:00 AM Expires: 11/1/2025 12:00:00 AM
Basically if I understand your problem correctly, you can solve it by simply adding 1 or 3 years to the MixDate year and then subtracting 30 days. This would still cause an issue counting backwards through February 29 if the product expires between March 1 and March 29 on a leap year, but it eliminates the overwhelming majority of the issue since 11/12 cases will only count forward through February 29. Hope this helps!

Get same day of same week in last year

I was looking for a way to fetch the same day of the current week as a year ago. For example, today is:
August 10th 2022 - Wednesday.
Assume this is the check-in date, the check-out date I expect to get is:
August 11, 2021 - Wednesday.
Because it's the same day (Wednesday) as last year. But I need to take leap years into account, so I need to see if the current year is a leap year and if it is, if it has passed the 29th of February, the same with the date last year.
How to do this using .net core ? I thought of something like:
private DateTime GetDayOneYearBefore()
{
if(DateTime.IsLeapYear(DateTime.Today.Year) && DateTime.Today.Month > 2){
return DateTime.Today.AddDays(-365);
}
else if(DateTime.IsLeapYear(DateTime.Today.Year) && DateTime.Today.Month <= 2){
return DateTime.Today.AddDays(-364);
}
}
Since you mention the "same week" I suppose you want to get the same day of the week in the same week number?
If so, you can do the following:
// In the System.DayOfWeek enum Sunday = 0, while Monday = 1
// This converts DateTime.DayOfWeek to a range where Monday = 0 and Sunday = 6
static int DayOfWeek(DateTime dt)
{
const int weekStart = (int)System.DayOfWeek.Monday;
const int daysInAWeek = 7;
return (daysInAWeek - (weekStart - (int)dt.DayOfWeek)) % daysInAWeek;
}
var calendar = CultureInfo.CurrentCulture.Calendar;
var weekNum = calendar.GetWeekOfYear(DateTime.Today, CalendarWeekRule.FirstFourDayWeek, System.DayOfWeek.Monday);
var todayLastYear = DateTime.Today.AddYears(-1);
var lastYearWeekNum = calendar.GetWeekOfYear(todayLastYear, CalendarWeekRule.FirstFourDayWeek, System.DayOfWeek.Monday);
var sameWeekLastYear = todayLastYear.AddDays(7 * (weekNum - lastYearWeekNum));
var sameDaySameWeekLastYear = sameWeekLastYear.AddDays(DayOfWeek(DateTime.Today) - DayOfWeek(sameWeekLastYear));
As you might notice there's a little convertion method, since I normally work with Monday being the first day of the week. If you prefer a different day to be the first day of the week, simply replace System.DayOfWeek.Monday with which ever day you'd like.
See this fiddle for a test run.

Humanizer for DateTime

I have this code:
Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(.75);
var dateTime1 = DateTime.UtcNow.AddYears(2).AddMonths(-5);
var text1 = dateTime1.Humanize();
In the text1 variable I get "one year from now". But this is not very accurate. Is there any way to get "one year and seven months from now"?
Update 1:
Solution #Daniel Hoffman has some problems, for example if my date is in the past:
//UtcNow is 11.07.2021
var dateTime6 = new DateTime(2021, 4, 24);
TimeSpan dateTimeSpan6 = dateTime6 - DateTime.UtcNow;
var text6 = dateTime6.Humanize();
string textSpan6 = dateTimeSpan6.Humanize(maxUnit: TimeUnit.Year, precision: 2);
then I get "2 months, 11 weeks" which contains basically the same information twice but in different units.
Update 2:
I have fixed the problem with dates in the past, by using Duration() method:
var timeSpan = date - DateTime.UtcNow;
return timeSpan.Duration().Humanize(maxUnit: TimeUnit.Year, precision: 2, minUnit: TimeUnit.Day);
[Edit]: Using TimeSpan will allow you to specify the precision of your period, but you will lose the ability to have "yesterday" or
"tomorrow", and it omits the " ago" or " from now", all of which are
localized.
A partial workaround would be to use the TimeSpan.Humanize
method for TimeSpans less than 366 days and DateTime.Humanize
otherwise. And if it's only going to be used in one language, the user
can append the appropriate text depending on if the timespan is
negative.
You can use the precision parameter with a TimeSpan:
TimeSpan periodFromNow = DateTime.UtcNow.AddYears(2).AddMonths(-5) - DateTime.UtcNow;
Then:
string myPeriodFromNow = periodFromNow.Humanize(maxUnit: TimeUnit.Year, precision: 2);
Other examples:
TimeSpan.FromDays(486).Humanize(maxUnit: TimeUnit.Year, precision: 7) => "1 year, 3 months, 29 days" // One day further is 1 year, 4 month
TimeSpan.FromDays(517).Humanize(maxUnit: TimeUnit.Year, precision: 7) => "1 year, 4 months, 30 days" // This month has 30 days and one day further is 1 year, 5 months
See also: https://github.com/Humanizr/Humanizer#humanize-timespan
It seems like its not currently possible in Humanizer to do what you want.
Check out this method PrecisionHumanize() on line 102, if the amount of days exceeds 365 then only years will be returned. And in general it seems like only one type of length of time can be returned, there is no years and months or minutes and seconds, just the largest one.
But check out another library called NodaTime it might be able to do what you want.
Here is a link to a different question similar to yours.

Can calculate age with user input in C#, but don't know why it works

So I used a solution found on a question from a different user here, link: How do I calculate someone's age in C#?, where the DOB was hardcoded, I added the code to take in a user input from the console to then calculate the age, however I don't know why the if statement produces a correct result, without it, it calculates the age you'll be that year, not your actual age.
var today = DateTime.Today;
Console.WriteLine("Type in your D.O.B in DD-MM-YYYY format:");
var Bday = Console.ReadLine();
var myDate = Convert.ToDateTime(Bday);
var age = today.Year - myDate.Year;
if (myDate.Date > today.AddYears(-age)) age--;
Console.WriteLine($"You are {age} years old");
Console.ReadLine();
Any help will be greatly appreciated!
You are asking about how this line works:
if (myDate.Date > today.AddYears(-age)) age--;
The number of years has been calculated by subtracting the year from the DOB from the year now. For example, if your DOB was 15/05/1979 and now is 2020 this would give:
age = 2020 - 1979 = 41
However, if this person's DOB was after today, they would not be 41 yet, they would still be 40.
In the case of the if statement, if the date of their birth (the month and day) is after today, one year is subtracted from age to give the correct age (which would be 40 in my example).
The logic is not as concise as it could be, but essentially it says:
Take the calculated age in years from today's date (example: 15/05/1979 = 41 years, 12/01/2020 - 41 years = 12/01/1979)
Is the DOB after this value? (is 15/05/1979 AFTER 12/01/1979)
If yes, subtract 1 from age (example: 41 - 1 = 40 years)

c# find same range of "named days" from 1 year ago

using c# visual studio 2008.
Can anyone help with an algorithm to do this please
if i have a range of days selected for this week (eg monday to friday) i can find the dates for these using the datetime functions available.
What i want to do is compared to stored data for the same DAY range 1 year ago.
So basicly i need to go back 1 year and find the dates for the nearest Mon to fri DAY range from 1 year previous. I guess i also need to take into acount leap years.
Can anyone help with a suitable algorithm on how to achieve this.
Of course the DAY for todays date last year is not going to be the same day.
thanks in advance
Here's some code which might do what you want - but the test cases show that there are corner cases to consider:
using System;
public class Test
{
static void Main()
{
Console.WriteLine(SameDayLastYear(DateTime.Today));
Console.WriteLine(SameDayLastYear(new DateTime(2010, 12, 31)));
}
static DateTime SameDayLastYear(DateTime original)
{
DateTime sameDate = original.AddYears(-1);
int daysDiff = original.DayOfWeek - sameDate.DayOfWeek;
return sameDate.AddDays(daysDiff);
}
}
What would you want the result for the second call to be? This code returns January 1st 2010, because that's the closest date to "a year ago on the same day".
I strongly suggest that whatever you go with, you have unit tests checking leap years, start and end of year etc.
Let's say you select Wednesday 10-02-2010 - Friday 12-02-2010 this year.
Last year that would have been Tuesday 10-02-2009 - Thursday 12-02-2009.
So you can do the following: Go back a year by simply performing DateTime.AddYears(-1). Make sure you correct for leap years here.
Then you use .AddDays(1) until you end up on a Wednesday - Friday timeframe.
That way you only have to take leap years into account at one point and this should produce the result you need.
I just subtracted one year then ran backwards until I found a Monday. LastYear will end up being the first Monday before this date last year
DateTime LastYear = DateTime.Now.AddYears(-1)
DayOfWeek Check = LastYear.DayOfWeek;
while (Check != DayOfWeek.Monday)
{
LastYear = LastYear.addDays(-1);
Check = LastYear.DayOfWeek;
}
Console.WriteLine("{0}",LastYear);
DateTime now = DateTime.Now;
DateTime lastyear = now.AddYears(-1);
string dayOfWeek = lastyear.DayOfWeek.ToString();
if (dayOfWeek.Equals("Saturday")) { dayOfWeek = "Friday"; }
else if (dayOfWeek.Equals("Sunday")) { dayOfWeek = "Monday"; }
Console.WriteLine(dayOfWeek);
Console.ReadKey();
Get a datetime object for last year, then use the DayOfWeek property.
This was pretty fun.
// today's info
DateTime today = DateTime.Now;
DayOfWeek today_name = today.DayOfWeek;
// this day one year ago
DateTime year_ago = today - new TimeSpan( ((today.Year - 1) % 4) ? 365 : 366, 0, 0, 0);
// find the closest day to today's info's name
DayOfWeek today_name_a_year_ago = year_ago.DayOfWeek;
DateTime current_range_a_year_ago = year_ago - new TimeSpan( year_ago.DayOfWeek - today_name, 0, 0, 0);
Console.WriteLine( "Today is {0}, {1}", today_name, today);
Console.WriteLine( "One year from today was {0}, {1}", today_name_a_year_ago, year_ago);
Console.WriteLine( "New date range is {0}", current_range_a_year_ago);
I would highly recommend using the unit testing features built into VS2008 to make sure you account for corner cases.

Categories