Problem overview
The function isLeapYear receives a year as an int and returns true when it is a leap year and false otherwise. The rule has three layers:
- a year divisible by 4 is a leap year;
- except that a year divisible by 100 is not;
- except that a year divisible by 400 is, after all.
So 2020 and 1968 are leap years (divisible by 4, not by 100). 2021 is not (not divisible by 4). 1900 is not, because it is divisible by 100 but not by 400. 2000 is, because it is divisible by 400.
Approach
The rule has exceptions to exceptions, which is easy to get wrong if you test the conditions in the order they were written. The way out is to test from the most specific case to the most general, and to return as soon as one test decides the answer.
Checking 400 first means a year like 2000 is accepted before the century rule gets a chance to reject it. After that, a year that reaches the 100 test is a century year that failed the 400 test, so the answer is false. A year that reaches the 4 test is not a century year, so divisibility by 4 settles it. Anything left over is not divisible by 4, so it is false.
Each test relies on the ones before it having returned, which keeps every condition short.
Step by step
- Test divisibility by 400. If
year % 400 == 0, returntrue. The remainder operator%gives what is left after a division, so a remainder of 0 means the division is exact. - Test divisibility by 100. If
year % 100 == 0, returnfalse. Only century years that are not multiples of 400 reach this line, such as 1900 or 2100. - Test divisibility by 4. If
year % 4 == 0, returntrue. Years that reach this line are not multiples of 100, so the ordinary every-four-years rule applies. - Return false. If none of the three tests matched, the year is not divisible by 4.
The solution in Go
func isLeapYear(year int) bool {
if year % 400 == 0 {
return true
}
if year % 100 == 0 {
return false
}
if year % 4 == 0 {
return true
}
return false
}
Go details to notice:
- No else branches. Each
returnleaves the function, so the code after anifonly runs when its condition was false. The chain reads top to bottom as a list of rules. - Operator precedence.
%binds tighter than==, soyear % 400 == 0means(year % 400) == 0. The standard formatter,gofmt, writes it asyear%400 == 0, with the tight spacing showing the higher precedence. - Integer remainder. The result of
%takes the sign of the left operand in Go, so-7 % 4is-3. That cannot trip up this lesson, because a remainder of 0 is 0 whatever the sign. - The result is a
bool. The function can also be written as a single return:year%4 == 0 && (year%100 != 0 || year%400 == 0). This gives the same results as the step-by-step version, but the ordered version is easier to read and to extend.
Complexity
The function performs at most three remainder operations and comparisons, so time is O(1) and extra space is O(1).
Edge cases and common mistakes
- Century years. 1900 and 2100 are not leap years; 1600 and 2000 are. These are the years the order of the tests exists to handle.
- Testing 4 first. If the function returned
trueforyear % 4 == 0before anything else, 1900 would be reported as a leap year. - Testing 100 first. If the function returned
falsefor multiples of 100 first, 2000 would be reported as a non-leap year. - Using
=instead of==. In Go,=assigns and cannot appear in a condition, soif year % 4 = 0is a compile error. - Years before 1 and year 0. The rule is applied mechanically to any integer, so
isLeapYear(0)returnstrue. The calendar never had a year 0, but the lesson's tests do not depend on that.
Testing
isLeapYear(2020)givestrue.isLeapYear(2021)givesfalse.isLeapYear(1968)givestrue.isLeapYear(1900)givesfalse.isLeapYear(2000)givestrue.isLeapYear(2100)givesfalse.