What are some examples of leap year bugs?
Asked Answered
P

6

21

A leap year bug is a code defect that creates a problematic, unintended outcome when executed within the context of a leap year, typically within the proleptic Gregorian calendar system.

The years 2016, 2020, 2024, 2028, etc. are leap years.

There are two attributes that are unique to leap years:

  • Leap years have a February 29th, while common years do not.
  • Leap years have 366 days in total, while common years have only 365.

This post is intended to help others understand the nature of leap year bugs, what they look like in various languages, and how to correct them.

Leap year bugs typically fall into two impact categories:

  • Category 1: Those that lead to error conditions, such as exceptions, error return codes, uninitialized variables, or endless loops
  • Category 2: Those that lead to incorrect data, such as off-by-one problems in range queries or aggregation

With each answer, please indicate the programming language and/or platform, as well as the impact category as defined above. (Follow the template used by existing answers please.)

Please create one separate answer per language and type of defect, and vote for your favorite, especially those you have personally encountered (leaving comments with anecdotes where possible).

I will seed a few answers to get started, and update with additional examples over time.

Periphrasis answered 16/8, 2019 at 20:43 Comment(0)
P
8

.NET / C# - Construction from date parts

Impact Category 1

Defective Code

DateTime dt = DateTime.Now;
DateTime result = new DateTime(dt.Year + 1, dt.Month, dt.Day);

This code will work properly until dt becomes February 29th. Then, it will attempt to create a February 29th of a common year, which does not exist. The DateTime constructor will throw an ArgumentOutOfRangeException.

Variations include any form of DateTime or DateTimeOffset constructor that accepts year, month, and day parameters, when those values are derived from different sources or manipulated without regard to validity as a whole.

Corrected Code

DateTime dt = DateTime.Now;
DateTime result = dt.AddYears(1);

Common Variation - Birthdays (and other anniversaries)

One variation is when determining a user's current birthday without considering leaplings (persons born on February 29th). It also applies to other types of anniversaries, such as hire date, date of service, billing date, etc.

Defective Code

DateTime birthdayThisYear = new DateTime(DateTime.Now.Year, dob.Month, dob.Day);

This approach needs adjustment, such as the following which uses February 28th for common years. (Though, march 1st might be preferred depending on the use case.)

Corrected Code

int year = DateTime.Now.Year;
int month = dob.Month;
int day = dob.Day;
if (month == 2 && day == 29 && !DateTime.IsLeapYear(year))
    day--;

DateTime birthdayThisYear = new DateTime(year, month, day);

Corrected Code (alternative implementation)

DateTime birthdayThisYear = dob.AddYears(DateTime.Now.Year - dob.Year);
Periphrasis answered 16/8, 2019 at 20:43 Comment(0)
P
3

Python - Replacing the year

Impact Category 1

Defective Code

from datetime import date
today = date.today()
later = today.replace(year = today.year + 1)

This code will work properly until today becomes February 29th. Then, it will attempt to create a February 29th of a common year, which does not exist. The date constructor will raise an ValueError with the message "day is out of range for month".

Variations:

from datetime import date
today = date.today()
later = date(today.year + 1, today.month, today.day)

Corrected Code

Without using any additional libraries, one can trap the error:

from datetime import date
today = date.today()
try:
    later = today.replace(year = today.year + 1)
except ValueError:
    later = date(today.year + 1, 2, 28)

However, it is usually better to use a library such as dateutil:

from datetime import date
from dateutil.relativedelta import relativedelta
today = date.today()
later = today + relativedelta(years=1)
Periphrasis answered 16/8, 2019 at 20:43 Comment(0)
P
3

Win32 / C++ - SYSTEMTIME struct manipulation

Impact Category 1

Defective Code

SYSTEMTIME st;
FILETIME ft;

GetSystemTime(&st);
st.wYear++;

SystemTimeToFileTime(&st, &ft);

This code will work properly until st becomes February 29th. Then, it will attempt to create a February 29th of a common year, which does not exist. Passing this to any function that accepts a SYSTEMTIME struct will likely fail.

For example, the SystemTimeToFileTime call shown here will return an error code. Since that return value is unchecked (which is extremely common), this will result in ft being left uninitialized.

Corrected Code

SYSTEMTIME st;
FILETIME ft;

GetSystemTime(&st);
st.wYear++;

bool isLeapYear = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0);
st.wDay = st.wMonth == 2 && st.wDay == 29 && !isLeapYear ? 28 : st.wDay;

bool ok = SystemTimeToFileTime(&st, &ft);
if (!ok)
{
  // handle error
}

This fix checks for Feb 29th of a common year, and corrects it to Feb 28th.

Periphrasis answered 16/8, 2019 at 20:43 Comment(0)
P
1

Determining if a Year is a Leap Year

C# is used in examples to illustrate, but the pattern is common to all languages.

Impact Category 2

Defective Code

bool isLeapYear = year % 4 == 0;

This code incorrectly assumes that a leap year occurs exactly every four years. This is not the case with the Gregorian calendar system that we most commonly use in business and computing.

Corrected Code

The full algorithm (from Wikipedia) is as follows:

if (year is not divisible by 4) then (it is a common year)
else if (year is not divisible by 100) then (it is a leap year)
else if (year is not divisible by 400) then (it is a common year)
else (it is a leap year)

One implementation of this algorithm is as follows:

bool isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);

In many platforms, this function is built in. For example, in .Net, the following is preferred:

bool isLeapYear = DateTime.IsLeapyear(year);
Periphrasis answered 16/8, 2019 at 20:43 Comment(0)
A
1

Here's an interesting one I just came across. Just another argument to use UTC wherever possible.

// **** Appears to "Leap" forward **** //
moment('2020-02-29T00:00:00Z').toISOString();
// or
moment('2020-02-29T00:00:00Z').toJSON();
// 2020-02-29T00:00:00.000Z

moment('2020-02-29T00:00:00Z').add(1, 'year').toISOString();
// or
moment('2020-02-29T00:00:00Z').add(1, 'year').toJSON();
// 2021-03-01T00:00:00.000Z


// **** Falls back **** //
moment.utc('2020-02-29T00:00:00Z').toISOString();
// or
moment.utc('2020-02-29T00:00:00Z').toJSON();
// 2020-02-29T00:00:00.000Z

moment.utc('2020-02-29T00:00:00Z').add(1, 'year').toISOString();
// or
moment.utc('2020-02-29T00:00:00Z').add(1, 'year').toJSON();
// 2021-02-28T00:00:00.000Z

Given C#'s default behavior is to fall back...

DateTime dt = new DateTime(2020, 02, 29, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.AddYears(1);
// 2021-02-28T00:00:00.0000000Z

This could be crucial to ensure the front end and back end agree on whether to fall back or leap forward a day.

Autunite answered 16/12, 2019 at 19:35 Comment(2)
Interesting, but I think all that this demonstrates is that the precise amount of time being added for a year (or month or day) depends on what mode the moment object is in. In the first examples, the mode is still "local" even though you created it with the Z. Since toISOString also outputs in UTC, it's masked. If you either left the Z off, or used format for the output (or both), then the behavior would be clearer. (It's not really leaping forward.)Periphrasis
Very true, it was the underlying utc flag that lead me to using moment.utc() in my test. So perhaps this is more awareness regarding de/serialization over an API. And perhaps I used the wrong function to illustrate the context. It's an easy mistake for those not using moment.utc() when initializing dates from JSON and then back again.Autunite
P
0

JavaScript - Adding Year(s)

Impact Category 2

Defective Code

var dt = new Date();
dt.setFullYear(dt.getFullYear() + 1);

This code will work properly until dt becomes February 29th, such as on 2020-02-29. Then it will attempt to set the year to 2021. Since 2021-02-29 doesn't exist, the Date object will roll forward to the next valid date, which is 2020-03-01.

This may be the intended behavior in some cases. In other cases, being one day off may have negligible impact (such as an expiration date).

However, in many cases the intention when advancing a year is to stay roughly in the same position for the month and day. In other words, if you started at the end of February (2020-02-29) and advanced a year, you likely intended the result to also be and the end of February (2021-02-28), not at the start of March (2021-03-01).

Corrected Code

To add years in JavaScript while retaining an end-of-February behavior, use the following function.

function addYears(dt, n) {
  var m = dt.getMonth();
  dt.setFullYear(dt.getFullYear() + n);
  if (dt.getMonth() !== m)
    dt.setDate(dt.getDate() - 1);
}

// example usage
addYears(dt, 1);

Common Variation - Immutable Form

One may often have code that doesn't mutate the original object, such as the following:

Defective Code

var dt = new Date();
var result = new Date(dt.getFullYear() + 1, dt.getMonth(), dt.getDate());

The immutable form that retains end-of-February behavior is as follows:

Corrected Code

function addYears(dt, n) {
  var result = new Date(dt);
  result.setFullYear(result.getFullYear() + n);
  if (result.getMonth() !== dt.getMonth())
    result.setDate(result.getDate() - 1);
  return result;
}

// example usage
var result = addYears(dt, 1);

Libraries

JavaScript has many date/time libraries, such as Luxon, Date-Fns, Moment, and js-Joda. All of these libraries already use an end-of-February behavior for their add-years functions. No additional adjustment is necessary unless you desire a start-of-March behavior instead.

Periphrasis answered 16/8, 2019 at 20:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.