Many years ago I created the Fake Name Generator, a website that lets you create fake names, addresses, birth dates, and other profile information. I’ve had issues with the birth date component ever since I wrote the dang thing, including a huge bug I fixed a few years ago.
Well today I was informed of another bug. A while back I added the ability for visitors to select an age range for their generated identities. The code that handled this was atrocious: it would figure out the birth year range by subtracting the selected min/max ages from the current year, then pick a random year within that range. It would then pick a random number 1 to 12 to represent the month, a random number 1 to 31 to represent the day of the month, then it would use checkdate() to see if the date is valid (for example, 2/31/1990 would be invalid). If it is invalid, it would try again. Most of the possible generated dates are valid so it only occasional has to try again.
Aside from being ugly, the big problem with this code is that it doesn’t always generate birth dates within the selected age range. Say you want to generate a birth date that would make someone at least 19 years old and no older than 19 years old (i.e., exactly 19 years old). In January most of the generated birth dates will make an 18 year old, in June you’ll have about a 50/50 split of 18 and 19 year olds, and in December most will be 19 year olds. This is because my code was only dealing with the birth year, and wasn’t taking into account the birth day or month in relation to the current date. Not very noticeable if your age range is something like 1 to 100 years old, but very noticeable if it is January 10th and you want a birth date for a 19 year old. Like I said, horrible code.
So here is my latest try. This function takes a minimum and maximum age as parameters, and spits out an array with the birth date and age:
function dateTime($minAge = 19, $maxAge = 85)
{
// Sanity check
if(!is_numeric($minAge)) $minAge = 19;
if(!is_numeric($maxAge)) $maxAge = 85;
if($minAge > $maxAge) $minAge = $maxAge;
$tz = new DateTimeZone('US/Eastern');
$minDate = new DateTime('now', $tz);
$minDate->modify("-$maxAge years -1 year +1 day");
$maxDate = new DateTime('now', $tz);
$maxDate->modify("-$minAge years");
$minEpoch = $minDate->format('U');
$maxEpoch = $maxDate->format('U');
$birthday = new DateTime('@'.mt_rand($minEpoch, $maxEpoch), $tz);
$today = new DateTime('now', $tz);
$date = array(
'year' => $birthday->format('Y'),
'month' => $birthday->format('n'),
'month-name'=> $birthday->format('F'),
'day' => $birthday->format('j'),
'formatted' => $birthday->format('n/j/Y'),
'age' => $today->diff($birthday)->y,
);
return $date;
}
The code is a bit long, but is actually pretty straightforward:
First we do a little sanity check. I want my code to return valid data even if someone manages to pass in a bad value, so I check to make sure the min and max ages are numeric and that the min age isn’t larger than the max age. This could be improved, but I think it is good enough for my needs.
Next we create a DateTimeZone object. My server’s time is set to US Eastern, so I want all my dates to use this time zone. This object will get passed into the constructor for DateTime later in the code.
In the next block of code I create two DateTime objects with today’s date. I use the modify method to subtract an appropriate amount of time from each object so I get the minimum and maximum birth date that someone could have and still fall within the desired age range. DateTime makes this easy by allowing things like +1 day. It doesn’t care about plurals, so you can use day and days, year and years interchangeably and inappropriately.
This next bit was a stroke of genius (which I later discovered is what some experts online would consider “the obvious way”). I convert the minimum and maximum dates into Unix timestamps, which are just integers. So if we are generating an identity for someone between 20 and 50 years old, the minimum date would be something like -219961293 and the maximum would be 758259507.
Now that our dates are just integers, picking a random date between the two is easy. I create a new DateTime object with the date set to a random Unix timestamp between the minimum and maximum Unix timestamps I came up with in the previous step. I then create another DateTime object, $today, to use later to come up with the identity’s age.
I use an array to hold each bit of the birth date, a formatted version of the birth date, and the age. To get the parts of the birth date I just call the format method on my $birthday DateTime object. To get the age I use the diff method on my $today DateTime object to find the number of full years between the $birthday object and the $today object.
If all you want is to pick a random date between two dates, then this is obviously more than you need. You could probably get away with this, if you were sure that the input dates were valid:
function randomDate($startDate = 'now', $endDate = 'now')
{
$minDate = new DateTime($startDate);
$maxDate = new DateTime($endDate);
$minEpoch = $minDate->format('U');
$maxEpoch = $maxDate->format('U');
$randomDate = new DateTime('@'.mt_rand($minEpoch, $maxEpoch));
$today = new DateTime('now');
$date = array(
'year' => $randomDate->format('Y'),
'month' => $randomDate->format('n'),
'month-name'=> $randomDate->format('F'),
'day' => $randomDate->format('j'),
'formatted' => $randomDate->format('n/j/Y'),
'age' => $today->diff($randomDate)->y,
);
return $date;
}
DateTime can figure out most dates, so you could do something like randomDate(‘March 1, 1980’, ‘5/20/1999’). If you don’t pass any dates in then it will use today’s date.
I could probably re-use some of those DateTime objects, but I don’t think having a few of them matters enough to obfuscate the code. I could also just return the $birthday object rather than building an array to return, but my existing code expects an array.
I’m open to critiques and suggestions for improvements. I’m thick skinned so feel free to completely bash my code if you’d like. Or if you are having a problem with the code, please let me know and I’d be happy to try to help you out!