Working with Age: it's not the same as a TimeSpan!





5.00/5 (12 votes)
Working with an Age (as in a persons age) is not the same as a Timespan, and there is no simple way to return an age. This provides a class to solve this.
Introduction
Recently, I answered a QA question wanting to calculate age in Day, Month and Year format from the selected date and I thought "Simple - just subtract the two dates, to get a TimeSpan, and there is the info". Nope, TimeSpan doesn't support Months, or Years - so I returned it as a new DateTime object instead. Nope, that's wrong as well (as ProgamFOX[^] kindly and correctly pointed out) - a DateTime can't have a zero Month, but an age can.
Then Sergey Alexandrovich Kryukov[^] added this:
"There are no solution to this ill-posed problem at all. You should understand that months or years cannot be considered as the units of duration at all. Calendar is non-periodic, which is important in this case!"
Which got me thinking. I know what he means, but... I have an age, you have an age. Aren't they real? Can't they be represented? Yes - they can!
Background
Fortunately, there is a solution, but let's just look at why it's a problem:
The age of a bottle of wine depends on when it was manufactured, and the year in which the question is asked. Your age depends on when you were born and the date today - it will be different tomorrow. And as Adrian Mole will tell you, you can indeed be "13 and 3/4" or 13 years, 9 months old! And legally, there is a difference between (for example) 17 years, 11 months, and 30 days, and 18 years, zero months and zero days. The former may mean you are a child, the latter may mean you are to be treated as an adult, capable of making your own decisions - and accepting the punishment for them if necessary.
We have two fundamental Time handlers in .NET:
- DateTime which holds an absolute point in time.
- TimeSpan which holds a relative but fixed duration.
But an age is neither of these, nor is it a combination of one of each. The age of something is a fixed item which varies according to a second fixed point - it is not a fixed value in itself. It starts at a fixed point - the birthday, or commencement date - but it is only relevant when you add a second fixed point that the first can be compared to.
So, it was obvious I would need to do the job properly, and create an Age class to handle it.
The Age Class
It's pretty simple - it has a start and end date property, and Years, Months, and Days properties to read them out. I also added a number of constructor overloads to make it a bit more flexible.
The minimum constructor takes a DateTime as the start date, and uses the current date and time as the end date. It works out the number of years, months and days from them.
/// <summary>
/// Start of age period
/// Birthdate for example
/// </summary>
public DateTime StartDate
{
get { return _startDate; }
set
{
_startDate = value;
if (_endDate == null)
{
_endDate = DateTime.Now;
}
GetAge(_startDate, _endDate);
}
}
/// <summary>
/// End of age period
/// Today for example
/// </summary>
public DateTime EndDate
{
get { return _endDate; }
set
{
_endDate = value;
if (_startDate == null)
{
_startDate = DateTime.Now;
}
GetAge(_startDate, _endDate);
}
}
/// <summary>
/// Years between start and end dates
/// </summary>
public int Years { get; private set; }
/// <summary>
/// Months between start and end dates, not included in whole years
/// </summary>
public int Months { get; private set; }
/// <summary>
/// Days between start and end dates, not included in whole months
/// </summary>
public int Days { get; private set; }
/// <summary>
/// Is this an appointment?
/// Returns true if the end date is before the start date.
/// </summary>
public bool IsAppointment
{
get { return EndDate < StartDate; }
}
The IsAppointment property is to allow for cases when the start date is after the end date without having to return negative number of years, months and / or days which would complicate using the class.
I did consider making the StartDate property setter private
, but decided that was unnecessarily limiting if I did allow the end date to be changed. In the end I went for public getter and setter, with the setter re-calculating the age.
/// <summary>
/// Constructs an Age between the given start date and now
/// </summary>
/// <param name="startDate">Date to start: for example, birthdate</param>
public Age(DateTime startDate) : this(startDate, DateTime.Now.Date) { }
/// <summary>
/// Constructs an Age between the given start date and end date
/// </summary>
/// <param name="startDate">Date to start: for example, birthdate</param>
/// <param name="endDate">Date to end: for example, today</param>
public Age(DateTime startDate, DateTime endDate)
{
_startDate = startDate;
_endDate = endDate;
GetAge(startDate, endDate);
}
The GetAge method does the real work:/// <summary>
/// Calculate the age
/// </summary>
/// <param name="start">Date to start: for example, birthdate</param>
/// <param name="end">Date to end: for example, today</param>
private void GetAge(DateTime start, DateTime end)
{
if (start > end)
{
// Rationalize the inputs!
DateTime temp = start;
start = end;
end = temp;
}
Days = end.Day - start.Day;
if (Days < 0)
{
end = end.AddMonths(-1);
Days += DateTime.DaysInMonth(end.Year, end.Month);
}
Months = end.Month - start.Month;
if (Months < 0)
{
end = end.AddYears(-1);
Months += 12;
}
Years = end.Year - start.Year;
}
I also added a couple of constructors to make it easier to say "this date will be your nth birthday":/// <summary>
/// Constructs an Age based on a start date and a given number of years
/// </summary>
/// <param name="startDate">Date to start: for example, birthdate</param>
/// <param name="years">Number of years for the age</param>
public Age(DateTime startDate, int years) : this(startDate, years, 0, 0) { }
/// <summary>
/// Constructs an Age based on a start date and a given number of years and months
/// </summary>
/// <param name="startDate">Date to start: for example, birthdate</param>
/// <param name="years">Number of years for the age</param>
/// <param name="months">Number of months for the age</param>
public Age(DateTime startDate, int years, int months) : this(startDate, years, months, 0) { }
/// <summary>
/// Constructs an Age based on a start date and a given number of years, months and days
/// </summary>
/// <param name="startDate">Date to start: for example, birthdate</param>
/// <param name="years">Number of years for the age</param>
/// <param name="months">Number of months for the age</param>
/// <param name="days">Number of days for the age</param>
public Age(DateTime startDate, int years, int months, int days)
{
_startDate = startDate;
_endDate = _startDate.AddYears(years).AddMonths(months).AddDays(days);
GetAge(_startDate, _endDate);
}
A public Update method to change the end date to the current:/// <summary>
/// Updates the Age for the current date.
/// </summary>
/// <returns></returns>
public Age Update()
{
EndDate = DateTime.Now;
return this;
}
Override ToString and we're done:/// <summary>
/// Returns a human readable form of the Age
/// </summary>
/// <returns></returns>
public override string ToString()
{
return string.Format("{0}{1} years, {2} months and {3} days", IsAppointment ? "will be " : "", Years, Months, Days);
}
Using the code
Simple!
DateTime dtBirth = new DateTime(1917, 2, 24);
Age age = new Age(dtBirth);
Console.WriteLine("You are {0} years, {1} months and {2} days old", age.Years, age.Months, age.Days);
Console.WriteLine(age);
Prints:You are 96 years, 1 months and 6 days old
96 years, 1 months and 6 days
History
Original version
30 March 2013
- Fix typo: "tsit" for "this"
- Fix downloads - both links pointed to the same file.