I want to know is there a better/easier way to load users by birthday.
I have a User instance, which has properties, including this one:
public DateTime? BirthDate { get; set; }
What I want to do is to load that users who has bdays from one date to another:
public IEnumerable<User> LoadUsersByBirthday(DateTime from, DateTime into)
I don't care about birthday year, for example, if I have 3 users with birthdays:
LoadUsersByBirthday(new DateTime(2990, 3, 1), new DateTime(3013, 4, 15))
should return 2 users - 1st and 3rd.
My method looks like this:
public IEnumerable<User> LoadUsersByBirthday(DateTime from, DateTime into)
{
var days = DateTime.DaysInMonth(from.Year, from.Month);
var u1 = _unit.User.Load(u => ((DateTime) (u.BirthDate)).Month == from.Month
&& ((DateTime) (u.BirthDate)).Day >= from.Day
&& ((DateTime) (u.BirthDate)).Day <= days);
var u2 = _unit.User.Load(u => ((DateTime) (u.BirthDate)).Month > from.Month
&& ((DateTime) (u.BirthDate)).Month < into.Month);
var u3 = _unit.User.Load(u => ((DateTime) (u.BirthDate)).Month == into.Month
&& ((DateTime) (u.BirthDate)).Day <= into.Day);
return u1.Concat(u2).Concat(u3);
}
It works, but is there a better/easier way to do this?
This was harder to solve than expected, due to the problems caused by leap years and people who's birthdays are actually on a leap day.
Finally, I remembered I had already a method that calculates someone's age in years accurately, accounting for leap years and birthdays that occur on a leap year.
Given such a method, it's trivial to determine if someone has a birthday give a range of dates:
First, calculate their age one day BEFORE the start of the range. Next, calculate their age at the end of the range.
If the calculated ages differ, then they must have had a birthday in the specified range.
Putting it all together:
public IEnumerable<User> UsersByBirthday(IEnumerable<User> users, DateTime from, DateTime to)
{
if (to < from)
throw new ArgumentException("'from' must be at or before 'to'.");
return users.Where(user => IsBirthdayInRange(user.BirthDate, from, to));
}
public bool IsBirthdayInRange(DateTime birthday, DateTime from, DateTime to)
{
if (to < from)
throw new ArgumentException("'from' must be at or before 'to'.");
if (birthday > from)
throw new ArgumentException("'from' must be after 'birthday'");
return AgeInYears(birthday, from.AddDays(-1)) < AgeInYears(birthday, to);
}
/// <summary>Returns a person's age in years, accounting for leap years.</summary>
public static int AgeInYears(DateTime birthday, DateTime today)
{
// See here for why this works:
// http://social.msdn.microsoft.com/Forums/en-US/ba4a98af-aab3-4c59-bdee-611334e502f2/calculate-age-with-a-single-line-of-code-c
return ((today.Year - birthday.Year) * 372 + (today.Month - birthday.Month) * 31 + (today.Day - birthday.Day)) / 372;
}
Here's a little test for a leap day birthday:
DateTime test = new DateTime(2004, 2, 29); // Leap day.
var start = new DateTime(2005, 3, 1);
var end = new DateTime(2005, 6, 7);
Console.WriteLine(IsBirthdayInRange(test, start, end));
Using your _unit.User.Load()
:
public IEnumerable<User> LoadUsersByBirthday(DateTime from, DateTime into)
{
return _unit.User.Load(u => IsBirthdayInRange(u.Birthdate, from, into));
}
[EDIT]
There are two possibilities for handling a leap day birthday in a non-leap year: Treat it as Feb 28th, or treat it as Mar 1st.
The code above treats it as Mar 1st (as per UK regulations). If you want to parameterise the handling, you can add a leapDayMapsToFeb28
parameter to IsBirthdayInRange()
as follows:
public bool IsBirthdayInRange(DateTime birthday, DateTime from, DateTime to, bool leapDayMapsToFeb28)
{
if (to < from)
throw new ArgumentException("'from' must be at or before 'to'.");
if (birthday > from)
throw new ArgumentException("'from' must be after 'birthday'");
if (leapDayMapsToFeb28 && (birthday.Month == 2) && (birthday.Day == 29) && !DateTime.IsLeapYear(from.Year))
birthday = new DateTime(birthday.Year, birthday.Month, 28);
return AgeInYears(birthday, from.AddDays(-1)) < AgeInYears(birthday, to);
}
I think this should work:
public IEnumerable<User> UsersByBirthday(IEnumerable<User> users, DateTime from, DateTime to)
{
var fromNormalized = NormalizeDate(from);
var intoNormalized = NormalizeDate(into);
return users.Where(u => fromNormalized <= NormalizeDate(u.BirthDate) && NormalizeDate(u.BirthDate) <= intoNormalized);
}
public int NormalizeDate(DateTime date)
{
return date.Month * 100 + date.Day;
}
The NormalizeDate()
method returns an integer value representing the month and day that is easy to compare with. For September 3 it returns 903, for April 24 it returns 424.
No error handling for if from > to
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With