phobos/std/datetime/common.d
2017-05-05 21:47:02 +02:00

737 lines
25 KiB
D

// Written in the D programming language
/++
License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
Authors: Jonathan M Davis
Source: $(PHOBOSSRC std/datetime/_common.d)
+/
module std.datetime.common;
import core.time : TimeException;
import std.typecons : Flag;
/++
Exception type used by std.datetime. It's an alias to
$(REF TimeException, core,time). Either can be caught without concern about
which module it came from.
+/
alias DateTimeException = TimeException;
/++
Represents the 12 months of the Gregorian year (January is 1).
+/
enum Month : ubyte { jan = 1, ///
feb, ///
mar, ///
apr, ///
may, ///
jun, ///
jul, ///
aug, ///
sep, ///
oct, ///
nov, ///
dec ///
}
/++
Represents the 7 days of the Gregorian week (Sunday is 0).
+/
enum DayOfWeek : ubyte { sun = 0, ///
mon, ///
tue, ///
wed, ///
thu, ///
fri, ///
sat ///
}
/++
In some date calculations, adding months or years can cause the date to fall
on a day of the month which is not valid (e.g. February 29th 2001 or
June 31st 2000). If overflow is allowed (as is the default), then the month
will be incremented accordingly (so, February 29th 2001 would become
March 1st 2001, and June 31st 2000 would become July 1st 2000). If overflow
is not allowed, then the day will be adjusted to the last valid day in that
month (so, February 29th 2001 would become February 28th 2001 and
June 31st 2000 would become June 30th 2000).
AllowDayOverflow only applies to calculations involving months or years.
If set to $(D AllowDayOverflow.no), then day overflow is not allowed.
Otherwise, if set to $(D AllowDayOverflow.yes), then day overflow is
allowed.
+/
alias AllowDayOverflow = Flag!"allowDayOverflow";
/++
Array of the strings representing time units, starting with the smallest
unit and going to the largest. It does not include $(D "nsecs").
Includes $(D "hnsecs") (hecto-nanoseconds (100 ns)),
$(D "usecs") (microseconds), $(D "msecs") (milliseconds), $(D "seconds"),
$(D "minutes"), $(D "hours"), $(D "days"), $(D "weeks"), $(D "months"), and
$(D "years")
+/
immutable string[] timeStrings = ["hnsecs", "usecs", "msecs", "seconds", "minutes",
"hours", "days", "weeks", "months", "years"];
/++
Returns whether the given value is valid for the given unit type when in a
time point. Naturally, a duration is not held to a particular range, but
the values in a time point are (e.g. a month must be in the range of
1 - 12 inclusive).
Params:
units = The units of time to validate.
value = The number to validate.
+/
bool valid(string units)(int value) @safe pure nothrow
if (units == "months" ||
units == "hours" ||
units == "minutes" ||
units == "seconds")
{
static if (units == "months")
return value >= Month.jan && value <= Month.dec;
else static if (units == "hours")
return value >= 0 && value <= 23;
else static if (units == "minutes")
return value >= 0 && value <= 59;
else static if (units == "seconds")
return value >= 0 && value <= 59;
}
///
@safe unittest
{
assert(valid!"hours"(12));
assert(!valid!"hours"(32));
assert(valid!"months"(12));
assert(!valid!"months"(13));
}
/++
Returns whether the given day is valid for the given year and month.
Params:
units = The units of time to validate.
year = The year of the day to validate.
month = The month of the day to validate.
day = The day to validate.
+/
bool valid(string units)(int year, int month, int day) @safe pure nothrow
if (units == "days")
{
return day > 0 && day <= maxDay(year, month);
}
/++
Params:
units = The units of time to validate.
value = The number to validate.
file = The file that the $(LREF DateTimeException) will list if thrown.
line = The line number that the $(LREF DateTimeException) will list if
thrown.
Throws:
$(LREF DateTimeException) if $(D valid!units(value)) is false.
+/
void enforceValid(string units)(int value, string file = __FILE__, size_t line = __LINE__) @safe pure
if (units == "months" ||
units == "hours" ||
units == "minutes" ||
units == "seconds")
{
import std.format : format;
static if (units == "months")
{
if (!valid!units(value))
throw new DateTimeException(format("%s is not a valid month of the year.", value), file, line);
}
else static if (units == "hours")
{
if (!valid!units(value))
throw new DateTimeException(format("%s is not a valid hour of the day.", value), file, line);
}
else static if (units == "minutes")
{
if (!valid!units(value))
throw new DateTimeException(format("%s is not a valid minute of an hour.", value), file, line);
}
else static if (units == "seconds")
{
if (!valid!units(value))
throw new DateTimeException(format("%s is not a valid second of a minute.", value), file, line);
}
}
/++
Params:
units = The units of time to validate.
year = The year of the day to validate.
month = The month of the day to validate.
day = The day to validate.
file = The file that the $(LREF DateTimeException) will list if thrown.
line = The line number that the $(LREF DateTimeException) will list if
thrown.
Throws:
$(LREF DateTimeException) if $(D valid!"days"(year, month, day)) is false.
+/
void enforceValid(string units)
(int year, Month month, int day, string file = __FILE__, size_t line = __LINE__) @safe pure
if (units == "days")
{
import std.format : format;
if (!valid!"days"(year, month, day))
throw new DateTimeException(format("%s is not a valid day in %s in %s", day, month, year), file, line);
}
/++
Returns the number of days from the current day of the week to the given
day of the week. If they are the same, then the result is 0.
Params:
currDoW = The current day of the week.
dow = The day of the week to get the number of days to.
+/
int daysToDayOfWeek(DayOfWeek currDoW, DayOfWeek dow) @safe pure nothrow
{
if (currDoW == dow)
return 0;
if (currDoW < dow)
return dow - currDoW;
return DayOfWeek.sat - currDoW + dow + 1;
}
@safe unittest
{
assert(daysToDayOfWeek(DayOfWeek.sun, DayOfWeek.sun) == 0);
assert(daysToDayOfWeek(DayOfWeek.sun, DayOfWeek.mon) == 1);
assert(daysToDayOfWeek(DayOfWeek.sun, DayOfWeek.tue) == 2);
assert(daysToDayOfWeek(DayOfWeek.sun, DayOfWeek.wed) == 3);
assert(daysToDayOfWeek(DayOfWeek.sun, DayOfWeek.thu) == 4);
assert(daysToDayOfWeek(DayOfWeek.sun, DayOfWeek.fri) == 5);
assert(daysToDayOfWeek(DayOfWeek.sun, DayOfWeek.sat) == 6);
assert(daysToDayOfWeek(DayOfWeek.mon, DayOfWeek.sun) == 6);
assert(daysToDayOfWeek(DayOfWeek.mon, DayOfWeek.mon) == 0);
assert(daysToDayOfWeek(DayOfWeek.mon, DayOfWeek.tue) == 1);
assert(daysToDayOfWeek(DayOfWeek.mon, DayOfWeek.wed) == 2);
assert(daysToDayOfWeek(DayOfWeek.mon, DayOfWeek.thu) == 3);
assert(daysToDayOfWeek(DayOfWeek.mon, DayOfWeek.fri) == 4);
assert(daysToDayOfWeek(DayOfWeek.mon, DayOfWeek.sat) == 5);
assert(daysToDayOfWeek(DayOfWeek.tue, DayOfWeek.sun) == 5);
assert(daysToDayOfWeek(DayOfWeek.tue, DayOfWeek.mon) == 6);
assert(daysToDayOfWeek(DayOfWeek.tue, DayOfWeek.tue) == 0);
assert(daysToDayOfWeek(DayOfWeek.tue, DayOfWeek.wed) == 1);
assert(daysToDayOfWeek(DayOfWeek.tue, DayOfWeek.thu) == 2);
assert(daysToDayOfWeek(DayOfWeek.tue, DayOfWeek.fri) == 3);
assert(daysToDayOfWeek(DayOfWeek.tue, DayOfWeek.sat) == 4);
assert(daysToDayOfWeek(DayOfWeek.wed, DayOfWeek.sun) == 4);
assert(daysToDayOfWeek(DayOfWeek.wed, DayOfWeek.mon) == 5);
assert(daysToDayOfWeek(DayOfWeek.wed, DayOfWeek.tue) == 6);
assert(daysToDayOfWeek(DayOfWeek.wed, DayOfWeek.wed) == 0);
assert(daysToDayOfWeek(DayOfWeek.wed, DayOfWeek.thu) == 1);
assert(daysToDayOfWeek(DayOfWeek.wed, DayOfWeek.fri) == 2);
assert(daysToDayOfWeek(DayOfWeek.wed, DayOfWeek.sat) == 3);
assert(daysToDayOfWeek(DayOfWeek.thu, DayOfWeek.sun) == 3);
assert(daysToDayOfWeek(DayOfWeek.thu, DayOfWeek.mon) == 4);
assert(daysToDayOfWeek(DayOfWeek.thu, DayOfWeek.tue) == 5);
assert(daysToDayOfWeek(DayOfWeek.thu, DayOfWeek.wed) == 6);
assert(daysToDayOfWeek(DayOfWeek.thu, DayOfWeek.thu) == 0);
assert(daysToDayOfWeek(DayOfWeek.thu, DayOfWeek.fri) == 1);
assert(daysToDayOfWeek(DayOfWeek.thu, DayOfWeek.sat) == 2);
assert(daysToDayOfWeek(DayOfWeek.fri, DayOfWeek.sun) == 2);
assert(daysToDayOfWeek(DayOfWeek.fri, DayOfWeek.mon) == 3);
assert(daysToDayOfWeek(DayOfWeek.fri, DayOfWeek.tue) == 4);
assert(daysToDayOfWeek(DayOfWeek.fri, DayOfWeek.wed) == 5);
assert(daysToDayOfWeek(DayOfWeek.fri, DayOfWeek.thu) == 6);
assert(daysToDayOfWeek(DayOfWeek.fri, DayOfWeek.fri) == 0);
assert(daysToDayOfWeek(DayOfWeek.fri, DayOfWeek.sat) == 1);
assert(daysToDayOfWeek(DayOfWeek.sat, DayOfWeek.sun) == 1);
assert(daysToDayOfWeek(DayOfWeek.sat, DayOfWeek.mon) == 2);
assert(daysToDayOfWeek(DayOfWeek.sat, DayOfWeek.tue) == 3);
assert(daysToDayOfWeek(DayOfWeek.sat, DayOfWeek.wed) == 4);
assert(daysToDayOfWeek(DayOfWeek.sat, DayOfWeek.thu) == 5);
assert(daysToDayOfWeek(DayOfWeek.sat, DayOfWeek.fri) == 6);
assert(daysToDayOfWeek(DayOfWeek.sat, DayOfWeek.sat) == 0);
}
/++
Returns the number of months from the current months of the year to the
given month of the year. If they are the same, then the result is 0.
Params:
currMonth = The current month of the year.
month = The month of the year to get the number of months to.
+/
int monthsToMonth(int currMonth, int month) @safe pure
{
enforceValid!"months"(currMonth);
enforceValid!"months"(month);
if (currMonth == month)
return 0;
if (currMonth < month)
return month - currMonth;
return Month.dec - currMonth + month;
}
@safe unittest
{
assert(monthsToMonth(Month.jan, Month.jan) == 0);
assert(monthsToMonth(Month.jan, Month.feb) == 1);
assert(monthsToMonth(Month.jan, Month.mar) == 2);
assert(monthsToMonth(Month.jan, Month.apr) == 3);
assert(monthsToMonth(Month.jan, Month.may) == 4);
assert(monthsToMonth(Month.jan, Month.jun) == 5);
assert(monthsToMonth(Month.jan, Month.jul) == 6);
assert(monthsToMonth(Month.jan, Month.aug) == 7);
assert(monthsToMonth(Month.jan, Month.sep) == 8);
assert(monthsToMonth(Month.jan, Month.oct) == 9);
assert(monthsToMonth(Month.jan, Month.nov) == 10);
assert(monthsToMonth(Month.jan, Month.dec) == 11);
assert(monthsToMonth(Month.may, Month.jan) == 8);
assert(monthsToMonth(Month.may, Month.feb) == 9);
assert(monthsToMonth(Month.may, Month.mar) == 10);
assert(monthsToMonth(Month.may, Month.apr) == 11);
assert(monthsToMonth(Month.may, Month.may) == 0);
assert(monthsToMonth(Month.may, Month.jun) == 1);
assert(monthsToMonth(Month.may, Month.jul) == 2);
assert(monthsToMonth(Month.may, Month.aug) == 3);
assert(monthsToMonth(Month.may, Month.sep) == 4);
assert(monthsToMonth(Month.may, Month.oct) == 5);
assert(monthsToMonth(Month.may, Month.nov) == 6);
assert(monthsToMonth(Month.may, Month.dec) == 7);
assert(monthsToMonth(Month.oct, Month.jan) == 3);
assert(monthsToMonth(Month.oct, Month.feb) == 4);
assert(monthsToMonth(Month.oct, Month.mar) == 5);
assert(monthsToMonth(Month.oct, Month.apr) == 6);
assert(monthsToMonth(Month.oct, Month.may) == 7);
assert(monthsToMonth(Month.oct, Month.jun) == 8);
assert(monthsToMonth(Month.oct, Month.jul) == 9);
assert(monthsToMonth(Month.oct, Month.aug) == 10);
assert(monthsToMonth(Month.oct, Month.sep) == 11);
assert(monthsToMonth(Month.oct, Month.oct) == 0);
assert(monthsToMonth(Month.oct, Month.nov) == 1);
assert(monthsToMonth(Month.oct, Month.dec) == 2);
assert(monthsToMonth(Month.dec, Month.jan) == 1);
assert(monthsToMonth(Month.dec, Month.feb) == 2);
assert(monthsToMonth(Month.dec, Month.mar) == 3);
assert(monthsToMonth(Month.dec, Month.apr) == 4);
assert(monthsToMonth(Month.dec, Month.may) == 5);
assert(monthsToMonth(Month.dec, Month.jun) == 6);
assert(monthsToMonth(Month.dec, Month.jul) == 7);
assert(monthsToMonth(Month.dec, Month.aug) == 8);
assert(monthsToMonth(Month.dec, Month.sep) == 9);
assert(monthsToMonth(Month.dec, Month.oct) == 10);
assert(monthsToMonth(Month.dec, Month.nov) == 11);
assert(monthsToMonth(Month.dec, Month.dec) == 0);
}
/++
Whether the given Gregorian Year is a leap year.
Params:
year = The year to to be tested.
+/
bool yearIsLeapYear(int year) @safe pure nothrow
{
if (year % 400 == 0)
return true;
if (year % 100 == 0)
return false;
return year % 4 == 0;
}
@safe unittest
{
import std.format : format;
foreach (year; [1, 2, 3, 5, 6, 7, 100, 200, 300, 500, 600, 700, 1998, 1999,
2001, 2002, 2003, 2005, 2006, 2007, 2009, 2010, 2011])
{
assert(!yearIsLeapYear(year), format("year: %s.", year));
assert(!yearIsLeapYear(-year), format("year: %s.", year));
}
foreach (year; [0, 4, 8, 400, 800, 1600, 1996, 2000, 2004, 2008, 2012])
{
assert(yearIsLeapYear(year), format("year: %s.", year));
assert(yearIsLeapYear(-year), format("year: %s.", year));
}
}
/++
Whether the given type defines all of the necessary functions for it to
function as a time point.
1. $(D T) must define a static property named $(D min) which is the smallest
value of $(D T) as $(Unqual!T).
2. $(D T) must define a static property named $(D max) which is the largest
value of $(D T) as $(Unqual!T).
3. $(D T) must define an $(D opBinary) for addition and subtraction that
accepts $(REF Duration, core,time) and returns $(D Unqual!T).
4. $(D T) must define an $(D opOpAssign) for addition and subtraction that
accepts $(REF Duration, core,time) and returns $(D ref Unqual!T).
5. $(D T) must define a $(D opBinary) for subtraction which accepts $(D T)
and returns returns $(REF Duration, core,time).
+/
template isTimePoint(T)
{
import core.time : Duration;
import std.traits : FunctionAttribute, functionAttributes, Unqual;
enum isTimePoint = hasMin &&
hasMax &&
hasOverloadedOpBinaryWithDuration &&
hasOverloadedOpAssignWithDuration &&
hasOverloadedOpBinaryWithSelf &&
!is(U == Duration);
private:
alias U = Unqual!T;
enum hasMin = __traits(hasMember, T, "min") &&
is(typeof(T.min) == U) &&
is(typeof({static assert(__traits(isStaticFunction, T.min));}));
enum hasMax = __traits(hasMember, T, "max") &&
is(typeof(T.max) == U) &&
is(typeof({static assert(__traits(isStaticFunction, T.max));}));
enum hasOverloadedOpBinaryWithDuration = is(typeof(T.init + Duration.init) == U) &&
is(typeof(T.init - Duration.init) == U);
enum hasOverloadedOpAssignWithDuration = is(typeof(U.init += Duration.init) == U) &&
is(typeof(U.init -= Duration.init) == U) &&
is(typeof(
{
// Until the overload with TickDuration is removed, this is ambiguous.
//alias add = U.opOpAssign!"+";
//alias sub = U.opOpAssign!"-";
U u;
auto ref add() { return u += Duration.init; }
auto ref sub() { return u -= Duration.init; }
alias FA = FunctionAttribute;
static assert((functionAttributes!add & FA.ref_) != 0);
static assert((functionAttributes!sub & FA.ref_) != 0);
}));
enum hasOverloadedOpBinaryWithSelf = is(typeof(T.init - T.init) == Duration);
}
///
@safe unittest
{
import core.time : Duration;
import std.datetime.date : Date;
import std.datetime.datetime : DateTime;
import std.datetime.interval : Interval;
import std.datetime.systime : SysTime;
import std.datetime.timeofday : TimeOfDay;
static assert(isTimePoint!Date);
static assert(isTimePoint!DateTime);
static assert(isTimePoint!SysTime);
static assert(isTimePoint!TimeOfDay);
static assert(!isTimePoint!int);
static assert(!isTimePoint!Duration);
static assert(!isTimePoint!(Interval!SysTime));
}
@safe unittest
{
import core.time;
import std.datetime.date;
import std.datetime.datetime;
import std.datetime.interval;
import std.datetime.systime;
import std.datetime.timeofday;
import std.meta : AliasSeq;
foreach (TP; AliasSeq!(Date, DateTime, SysTime, TimeOfDay))
{
static assert(isTimePoint!(const TP), TP.stringof);
static assert(isTimePoint!(immutable TP), TP.stringof);
}
foreach (T; AliasSeq!(float, string, Duration, Interval!Date, PosInfInterval!Date, NegInfInterval!Date))
static assert(!isTimePoint!T, T.stringof);
}
/++
Whether all of the given strings are valid units of time.
$(D "nsecs") is not considered a valid unit of time. Nothing in std.datetime
can handle precision greater than hnsecs, and the few functions in core.time
which deal with "nsecs" deal with it explicitly.
+/
bool validTimeUnits(string[] units...) @safe pure nothrow
{
import std.algorithm.searching : canFind;
foreach (str; units)
{
if (!canFind(timeStrings[], str))
return false;
}
return true;
}
/++
Compares two time unit strings. $(D "years") are the largest units and
$(D "hnsecs") are the smallest.
Returns:
$(BOOKTABLE,
$(TR $(TD this &lt; rhs) $(TD &lt; 0))
$(TR $(TD this == rhs) $(TD 0))
$(TR $(TD this &gt; rhs) $(TD &gt; 0))
)
Throws:
$(LREF DateTimeException) if either of the given strings is not a valid
time unit string.
+/
int cmpTimeUnits(string lhs, string rhs) @safe pure
{
import std.algorithm.searching : countUntil;
import std.exception : enforce;
import std.format : format;
auto tstrings = timeStrings;
immutable indexOfLHS = countUntil(tstrings, lhs);
immutable indexOfRHS = countUntil(tstrings, rhs);
enforce(indexOfLHS != -1, format("%s is not a valid TimeString", lhs));
enforce(indexOfRHS != -1, format("%s is not a valid TimeString", rhs));
if (indexOfLHS < indexOfRHS)
return -1;
if (indexOfLHS > indexOfRHS)
return 1;
return 0;
}
@safe unittest
{
foreach (i, outerUnits; timeStrings)
{
assert(cmpTimeUnits(outerUnits, outerUnits) == 0);
// For some reason, $ won't compile.
foreach (innerUnits; timeStrings[i + 1 .. timeStrings.length])
assert(cmpTimeUnits(outerUnits, innerUnits) == -1);
}
foreach (i, outerUnits; timeStrings)
{
foreach (innerUnits; timeStrings[0 .. i])
assert(cmpTimeUnits(outerUnits, innerUnits) == 1);
}
}
/++
Compares two time unit strings at compile time. $(D "years") are the largest
units and $(D "hnsecs") are the smallest.
This template is used instead of $(D cmpTimeUnits) because exceptions
can't be thrown at compile time and $(D cmpTimeUnits) must enforce that
the strings it's given are valid time unit strings. This template uses a
template constraint instead.
Returns:
$(BOOKTABLE,
$(TR $(TD this &lt; rhs) $(TD &lt; 0))
$(TR $(TD this == rhs) $(TD 0))
$(TR $(TD this &gt; rhs) $(TD &gt; 0))
)
+/
template CmpTimeUnits(string lhs, string rhs)
if (validTimeUnits(lhs, rhs))
{
enum CmpTimeUnits = cmpTimeUnitsCTFE(lhs, rhs);
}
/+
Helper function for $(D CmpTimeUnits).
+/
private int cmpTimeUnitsCTFE(string lhs, string rhs) @safe pure nothrow
{
import std.algorithm.searching : countUntil;
auto tstrings = timeStrings;
immutable indexOfLHS = countUntil(tstrings, lhs);
immutable indexOfRHS = countUntil(tstrings, rhs);
if (indexOfLHS < indexOfRHS)
return -1;
if (indexOfLHS > indexOfRHS)
return 1;
return 0;
}
@safe unittest
{
import std.format : format;
import std.meta : AliasSeq;
static string genTest(size_t index)
{
auto currUnits = timeStrings[index];
auto test = format(`assert(CmpTimeUnits!("%s", "%s") == 0);`, currUnits, currUnits);
foreach (units; timeStrings[index + 1 .. $])
test ~= format(`assert(CmpTimeUnits!("%s", "%s") == -1);`, currUnits, units);
foreach (units; timeStrings[0 .. index])
test ~= format(`assert(CmpTimeUnits!("%s", "%s") == 1);`, currUnits, units);
return test;
}
static assert(timeStrings.length == 10);
foreach (n; AliasSeq!(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))
mixin(genTest(n));
}
//==============================================================================
// Section with package-level helper functions and templates.
//==============================================================================
package:
/+
Array of the short (three letter) names of each month.
+/
immutable string[12] _monthNames = ["Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"];
/+
The maximum valid Day in the given month in the given year.
Params:
year = The year to get the day for.
month = The month of the Gregorian Calendar to get the day for.
+/
ubyte maxDay(int year, int month) @safe pure nothrow
in
{
assert(valid!"months"(month));
}
body
{
switch (month)
{
case Month.jan, Month.mar, Month.may, Month.jul, Month.aug, Month.oct, Month.dec:
return 31;
case Month.feb:
return yearIsLeapYear(year) ? 29 : 28;
case Month.apr, Month.jun, Month.sep, Month.nov:
return 30;
default:
assert(0, "Invalid month.");
}
}
@safe unittest
{
// Test A.D.
assert(maxDay(1999, 1) == 31);
assert(maxDay(1999, 2) == 28);
assert(maxDay(1999, 3) == 31);
assert(maxDay(1999, 4) == 30);
assert(maxDay(1999, 5) == 31);
assert(maxDay(1999, 6) == 30);
assert(maxDay(1999, 7) == 31);
assert(maxDay(1999, 8) == 31);
assert(maxDay(1999, 9) == 30);
assert(maxDay(1999, 10) == 31);
assert(maxDay(1999, 11) == 30);
assert(maxDay(1999, 12) == 31);
assert(maxDay(2000, 1) == 31);
assert(maxDay(2000, 2) == 29);
assert(maxDay(2000, 3) == 31);
assert(maxDay(2000, 4) == 30);
assert(maxDay(2000, 5) == 31);
assert(maxDay(2000, 6) == 30);
assert(maxDay(2000, 7) == 31);
assert(maxDay(2000, 8) == 31);
assert(maxDay(2000, 9) == 30);
assert(maxDay(2000, 10) == 31);
assert(maxDay(2000, 11) == 30);
assert(maxDay(2000, 12) == 31);
// Test B.C.
assert(maxDay(-1999, 1) == 31);
assert(maxDay(-1999, 2) == 28);
assert(maxDay(-1999, 3) == 31);
assert(maxDay(-1999, 4) == 30);
assert(maxDay(-1999, 5) == 31);
assert(maxDay(-1999, 6) == 30);
assert(maxDay(-1999, 7) == 31);
assert(maxDay(-1999, 8) == 31);
assert(maxDay(-1999, 9) == 30);
assert(maxDay(-1999, 10) == 31);
assert(maxDay(-1999, 11) == 30);
assert(maxDay(-1999, 12) == 31);
assert(maxDay(-2000, 1) == 31);
assert(maxDay(-2000, 2) == 29);
assert(maxDay(-2000, 3) == 31);
assert(maxDay(-2000, 4) == 30);
assert(maxDay(-2000, 5) == 31);
assert(maxDay(-2000, 6) == 30);
assert(maxDay(-2000, 7) == 31);
assert(maxDay(-2000, 8) == 31);
assert(maxDay(-2000, 9) == 30);
assert(maxDay(-2000, 10) == 31);
assert(maxDay(-2000, 11) == 30);
assert(maxDay(-2000, 12) == 31);
}