Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations John Tel on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Tutorial - Gregorian Calendar: Complete Guide To Leap Years

VB Programming Concepts

Tutorial - Gregorian Calendar: Complete Guide To Leap Years

by  Unscruffed  Posted    (Edited  )
INTRODUCTION

Since Tek-Tips has been most helpful to me over the years, I've put this together to help new Programmers (and maybe some old ones) with a matter that seems to crop up regularly. This Tutorial will provide a little history about the Gregorian Calendar explaining exactly when Leap Years occur, and provide several ways to evaluate whether or not a given Year is a Leap Year using Visual Basic. If you wish to obtain further information, the small "superscript" numbers refer to Wikipedia references at the bottom of the page.


HISTORY

Many people make the assumption that a Leap Year1 falls every 4th year, and with every 4th year in the last hundred or so being Leap Years, the assumption certainly rings true. With the Gregorian Calendar2 however, this assumption is false.

The Gregorian Calendar was introduced on the 24th of February 1582 by Pope Gregory VIII3, and is the Calendar that most of the world still uses today. It was introduced to restore the inaccuracy of the Julian Calendar4 (which had been in use since 45BC) which had accumulated a 10 day drift in relation to the Vernal Equinox5, and ultimately, the day Easter6 is celebrated.

The Gregorian Calendar addressed the drift by specifying a one-off 10 day "skip" to bring the Calendar back into synchronization with the seasons. This 10 day "skip" occured in October 1582, where the 4th was followed by the 15th.

The Calendar also adopted a new formula for Leap Years in order to keep the Calendar accurate without further modifications. The Leap Year formula was changed from the Julian specification of every 4 years to "every year that is exactly divisible by 4, excluding years that are exactly divisible by 100, but including years that are exactly divisible by 400".

It has since been suggested that a further adjustment to the Leap Year formula (see "The 4000 Proposal" below) would make the Calendar even more accurate, but this proposal has not been officially adopted. So for now, a Gregorian Leap Year must adhere to the following:
[tt]
[color blue]Be divisible by 4 AND ( NOT Be divisible by 100 OR Be divisible by 400 )[/color]
[/tt]
The best way to calculate this (we will see why later) is to reverse the formula, so that the divisors are in "highest to lowest" order. We therefore re-write the formula as:
[tt]
[color blue]( Be divisible by 400 OR NOT Be divisible by 100 ) AND Be divisible by 4[/color]
[/tt]
An example of how this affects when Leap Years occur, is shown in the list below. Note the Centurian Years 1500, 1600, 1700, 1800, 1900, 2000 and 2100.
[tt]
....
1496: True => NOT Divisible by 100
[color red]1500: False => NOT Divisible by 400[/color]
1504: True => NOT Divisible by 100
....
1596: True => NOT Divisible by 100
[color green]1600: True => Divisible by 400[/color]
1604: True => NOT Divisible by 100
....
1696: True => NOT Divisible by 100
[color red]1700: False => NOT Divisible by 400[/color]
1704: True => NOT Divisible by 100
....
1796: True => NOT Divisible by 100
[color red]1800: False => NOT Divisible by 400[/color]
1804: True => NOT Divisible by 100
....
1896: True => NOT Divisible by 100
[color red]1900: False => NOT Divisible by 400[/color]
1904: True => NOT Divisible by 100
....
1996: True => NOT Divisible by 100
[color green]2000: True => Divisible by 400[/color]
2004: True => NOT Divisible by 100
....
2096: True => NOT Divisible by 100
[color red]2100: False => NOT Divisible by 400[/color]
2104: True => NOT Divisible by 100
....
[/tt]
We can see from the list that the last "skipped" Leap Year was over one hundred years ago in 1900. The next skip won't occur until the year 2100.

The basics to remember are "400, 100, 4" and exit with "True, False, True". (or, 4 = True and 1 = False)


THE MATHEMATICS

Because all the divisors we are testing for are divisible by the divisors below them (ie: 400 is divisible by both 100 and 4; and 100 is divisible by 4), in mathematical terms, we can simply test for each divisor from highest to lowest, and exit with the response that we require at the first occurance of a remainder of zero.

Thus:
[tt]
Divide by 400: If the remainder = 0,
the Year is a Leap Year, so exit.
Otherwise:
Divide by 100: If the remainder = 0,
the Year is not a Leap Year, so exit.
Otherwise:
Divide by 4: If the remainder = 0,
the Year is a Leap Year, so exit.
Otherwise:
The Year is not a Leap Year.
[/tt]
This can be better expressed using modulus:
[tt]
If (Year mod 400) = 0: Exit True
If (Year mod 100) = 0: Exit False
If (Year mod 4) = 0: Exit True
Exit False
[/tt]
Example 1:
[tt]
1900 mod 400 = 300 Not zero, therefore goto next step...
1900 mod 100 = 0 Is zero, therefore 1900 is not a Leap Year.
[/tt]
Example 2:
[tt]
1951 mod 400 = 300 Not zero, therefore goto next step...
1951 mod 100 = 51 Not zero, therefore goto next step...
1951 mod 4 = 3 Not zero, therefore 1951 is not a Leap Year.
[/tt]
Example 3:
[tt]
1996 mod 400 = 396 Not zero, therefore goto next step...
1996 mod 100 = 96 Not zero, therefore goto next step...
1996 mod 4 = 0 Is zero, therefore 1996 is a Leap Year.
[/tt]
Example 4:
[tt]
2000 mod 400 = 0 Is zero, therefore 2000 is a Leap Year.
[/tt]


TRANSFORMING IT ALL TO CODE

There are a few different ways to transform the formula into Visual Basic code.

Method 1: Using "If - ElseIf - Then"
Code:
Function IsLeapYear1(iYear As Integer) As Boolean
    If (iYear Mod 400) = 0 Then
        IsLeapYear1 = True
    ElseIf (iYear Mod 100) = 0 Then
        IsLeapYear1 = False
    ElseIf (iYear Mod 4) = 0 Then
        IsLeapYear1 = True
    Else
        IsLeapYear1 = False
    End If
End Function
Method 2: Using "Select Case"
Code:
Function IsLeapYear2(iYear As Integer) As Boolean
    Select Case True
        Case ((iYear Mod 400) = 0): IsLeapYear2 = True
        Case ((iYear Mod 100) = 0): IsLeapYear2 = False
        Case ((iYear Mod 4) = 0): IsLeapYear2 = True
        Case Else: IsLeapYear2 = False
    End Select
End Function
Putting mathematics aside, the easiest (and most overlooked) way to test for Leap Years in code, is to use inbuilt functions and check the date one day prior to the 1st of March. In VB, we use the inbuilt DateAdd and Day functions. The only drawback here, is the limitation of VB's Date variable, which has a maximum value of 31/Dec/9999. For most applications, this won't present any problem, but it is a limitation all the same and should be respected as such when programming.

Method 3: Using "DateAdd"
Code:
Function IsLeapYear3(iYear As Integer) As Boolean
    Dim dDate As Date
    dDate = "1/March/" & iYear
    dDate = DateAdd("d", -1, dDate)
    If Day(dDate) = 29 Then
        IsLeapYear3 = True
    Else
        IsLeapYear3 = False
    End If
End Function
Now that we understand what to test for and why, we can simplify the whole process into one line of code in two different ways, as shown in Methods 4 and 5. Note that Method 5 still has the same limitations as Method 3 above.

Method 4:
Code:
Function IsLeapYear4(iYear As Integer) As Boolean
    IsLeapYear4 = (((iYear Mod 400) = 0) Or ((iYear Mod 100) <> 0)) And ((iYear Mod 4) = 0)
End Function
Method 5:
Code:
Function IsLeapYear5(iYear As Integer) As Boolean
    IsLeapYear5 = (Day(DateAdd("d", -1, "1/March/" & iYear)) = 29)
End Function

THE "4000" PROPOSAL

As mentioned earlier, it has been suggested that every 4000th year should be made a "common" year, thus making the calendar even more accurate. To date, this has not been adopted, and is unlikely to be adopted in our lifetime. (It would actually be more accurate to make every 8000th year a common year, rather than every 4000th.)

In the unlikely event that this modification is adopted some time soon, Methods 3 and 5 above which use the inbuilt DateAdd and Day functions, will be incorrect for any year divisible by 4000. In fact, many of VB's Date calculations will be inaccurate if they involve any date after 28th February 4000, depending on what you are calculating.

Methods 1, 2 & 4 however, can be easily modified to accept the new formula. Keeping in mind that we test from highest to lowest, and that 4000 (and 8000) are both evenly divisible by all the lower divisors, we can simply insert the new equation at the beginning of our existing routines as follows:

Method 6: Modified Method 1
Code:
Function IsLeapYear6(iYear As Integer) As Boolean
    [color red][b]If (iYear Mod 4000) = 0 Then
        IsLeapYear6 = False
    Else[/b][/color]If (iYear Mod 400) = 0 Then
        IsLeapYear6 = True
    ElseIf (iYear Mod 100) = 0 Then
        IsLeapYear6 = False
    ElseIf (iYear Mod 4) = 0 Then
        IsLeapYear6 = True
    Else
        IsLeapYear6 = False
    End If
End Function
Method 7: Modified Method 2
Code:
Function IsLeapYear7(iYear As Integer) As Boolean
    Select Case True
        [color red][b]Case ((iYear Mod 4000) = 0): IsLeapYear7 = False[/b][/color]
        Case ((iYear Mod 400) = 0): IsLeapYear7 = True
        Case ((iYear Mod 100) = 0): IsLeapYear7 = False
        Case ((iYear Mod 4) = 0): IsLeapYear7 = True
        Case Else: IsLeapYear7 = False
    End Select
End Function
Method 8: Modified Method 4
Code:
Function IsLeapYear8(iYear As Integer) As Boolean
    IsLeapYear8 =  [color red][b]((iYear Mod 4000) <> 0) And[/b][/color] (((iYear Mod 400) = 0) Or ((iYear Mod 100) <> 0)) And ((iYear Mod 4) = 0)
End Function

CONCLUSION

I leave you with this thought: Did you know that the extra day (called the "intercalary day"7) in February is actually the 24th and not the 29th? With this in mind, it's not just people born on the 29th who can brag about having one birthday every four years. Those born on the 24th, 25th, 26th, 27th, and 28th can also claim that their birthday doesn't fall on the correct day except in Leap Years, where an extra day is inserted at 24, pushing their "birthdays" to the correct position.

We really could go for hours on this, couldn't we?


REFERENCES

1[tab]Leap Day: [link http://en.wikipedia.org/wiki/Bissextile#Leap_day]http://en.wikipedia.org/wiki/Bissextile#Leap_day[/link]
2[tab]Gregorian Calendar: [link http://en.wikipedia.org/wiki/Gregorian_calendar]http://en.wikipedia.org/wiki/Gregorian_calendar[/link]
3[tab]Pope Gregory VIII: [link http://en.wikipedia.org/wiki/Pope_Gregory_XIII]http://en.wikipedia.org/wiki/Pope_Gregory_XIII[/link]
4[tab]Julian Calendar: [link http://en.wikipedia.org/wiki/Julian_calendar]http://en.wikipedia.org/wiki/Julian_calendar[/link]
5[tab]Vernal Equinox: [link http://en.wikipedia.org/wiki/Equinox]http://en.wikipedia.org/wiki/Equinox[/link]
6[tab]Easter: [link http://en.wikipedia.org/wiki/Easter]http://en.wikipedia.org/wiki/Easter[/link]
7[tab]Intercalary Day: [link http://en.wikipedia.org/wiki/Intercalation]http://en.wikipedia.org/wiki/Intercalation[/link]

Edited by Unscruffed 16th November 2011:
* Minor formatting and grammer edits.
* Corrected VB code - enclosed Mod calculations in parentheses.
Register to rate this FAQ  : BAD 1 2 3 4 5 6 7 8 9 10 GOOD
Please Note: 1 is Bad, 10 is Good :-)

Part and Inventory Search

Back
Top