Since you are in control of the input interface, without loss of generality we can assume that there will be separate year/month/day integers (properly validate for... being integer :). Let's say that year will be negative to indicate BC.
So first of all... the obvious (partial) answer: checkdate(). This is just fine for years >= 1, as the function documentation says.
You 're therefore stuck with the problem of what to do if year <= 0.
Let's make a side-trek here and see why that might be a BIG problem...
According to the Wikipedia link above, the Julian calendar came into effect in 45 BC. This calendar is, for all practical purposes, identical to the Gregorian calendar we use today. The difference is that there is a ten-day offset between them; the last day of the Julian calendar was Thursday, 4 October 1582 and this was followed by the first day of the Gregorian calendar, Friday, 15 October 1582 (the cycle of weekdays was not affected).
This already means that dates in the range 5 Oct 1582 to 14 Oct 1582 (inclusive) are invalid if you are following the Gregorian calendar; they have never existed.
Going backward from there, you 're good until 45 BC. From 46 BC backwards, the Roman calendar was used instead of the Julian.
I 'm not going to go into that mess here, but simply mention that since that calendar was quite different from the Gregorian, your users will not be prepared to see a "Roman calendar date input form". My suggestion is, better make your app usable than technically correct.
If it can be assumed that nobody in their right mind would actually know a BC date to the day, or know how to properly specify it even if they did, you might arbitrarily assume that all dates BC are of the form 1/1/YEAR. Your interface might therefore disable the month/day controls if a "BC" checkbox was checked, have separate group boxes for BC and AD, or anything else appropriate.
The only remaining problem after all this, as I see it, is checking dates for leap years. Those were introduced with the Julian calendar, but not actually implemented correctly until 8 AD.
The last link above documents that during 45 BC - 4 AD (inclusive) leap years were not calculated correctly. A is-year-leap function that accounts for that inconsistency, plus the julian/gregorian switch would be:
define('YEAR_JULIAN_CALENDAR_INTRODUCED', -45);
define('YEAR_JULIAN_CALENDAR_LEAP_IMPLEMENTED_CORRECTLY', 8);
define('YEAR_GREGORIAN_CALENDAR_INTRODUCED', 1582);
function is_leap_year($year) {
if($year < YEAR_JULIAN_CALENDAR_INTRODUCED) {
return false; // or good luck :)
}
if($year < YEAR_JULIAN_CALENDAR_LEAP_IMPLEMENTED_CORRECTLY) {
return $year <= -9 && $year % 3 == 0;
}
if($year < YEAR_GREGORIAN_CALENDAR_INTRODUCED) {
return $year % 4 == 0;
}
// Otherwise, Gregorian is in effect
return $year % 4 == 0 && ($year % 100 != 0 || $year % 400 == 0);
}
Armed with this, you could then write a function that correctly tells you how many days there are in each year. Date subtraction/addition could then be built on that.
After all this discussion (I do admire the courage of anyone who has read this far :) I have to ask:
How much accuracy do you actually need?
If you decide that you need to be anal about the "technical details", I would personally implement the functions mentioned above, and then: a) Use them as my handcrafted date library, or b) Use them to check that any 3-rd party library I 'm interested in is actually implemented correctly.
If you don't need to do that, just pretend you never read all this. :)