Rethinking Leap Years: Why Your Favorite Programming Language's Approach May Be Flawed

Rethinking Leap Years: Why Your Favorite Programming Language's Approach May Be Flawed

A historical mistake and how you can solve it

TL;DR: Most languages fail to find the correct behavior for leap year calculation.


Disclaimer: While I've tried my best to provide accurate insights across various programming languages, I acknowledge that I may not be an expert in everyone. If you spot an error or disagree with any points, please leave a respectful comment, and I'll promptly address it.

The State of the Art

Determining whether a year is a leap (or not) is a simple mathematical problem.

Every student can solve it as their first programming assignment.

To simplify the problem, let's assume a Year is leap when it is evenly divisible by 4, except if it's also divisible by 100, but it is a leap year if it's divisible by 400.

The real world and cosmic mechanics are a bit more complicated but it is beyond the scope of this article.

Let's explore how several programming languages solve this problem:

Horrible Approach

PHP:

<?php

$yearNumber = 2024;
$isLeap = date('L', mktime(0, 0, 0, 1, 1, $yearNumber));

SQL (PostgreSQL):

SELECT (EXTRACT(year FROM TIMESTAMP '2024-02-29') IS NOT NULL)
 AS is_leap_year;

These languages attempt to create a valid (or invalid) leap day and exploit truthy values.

This hack violates the fail-fast principle and abuses the billion-dollar mistake.

Trying to create an invalid date should throw an exception in serious languages since this happens in the real world domain.

Performing other actions like concealing errors beneath the surface breaches the principle of least astonishment.

Missing Behavior

Ada:

function Is_Leap_Year (Year : Integer) return Boolean is
begin
    return (Year mod 4 = 0 and then Year mod 100 /= 0) 
        or else (Year mod 400 = 0);
end Is_Leap_Year;

C/C++:

bool isLeapYear(int year) {
    return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

Go:

package main

import (
  "fmt"
  "time"
)

func isLeapYear(year int) bool {
  return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}

Haskell:

import Data.Time.Calendar (isLeapYear)
let year = 2024
let isLeap = isLeapYear year

JavaScript/TypeScript:

function isLeapYear(year) {
    return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}

Julia:

using Dates
year = 2024
isleap(year)

Lua:

local year = 2024
local isLeap = (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)

MATLAB:

year = 2024;
isLeap = mod(year, 4) == 0 && (mod(year, 100) ~= 0 || mod(year, 400) == 0);

Objective-C:

int yearNumber = 2024;
BOOL isLeap = (yearNumber % 4 == 0 && yearNumber % 100 != 0) 
  || (yearNumber % 400 == 0);

PowerShell:

$yearNumber = 2024
$isLeap = ($yearNumber % 4 -eq 0 -and $yearNumber % 100 -ne 0) 
  -or ($yearNumber % 400 -eq 0)

Rust:

fn is_leap_year(year: i32) -> bool {
    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}

Smalltalk:

| yearNumber |
yearNumber := 2024.
(yearNumber \\ 4 = 0)
  and: [(yearNumber \\ 100 ~= 0) or: [ yearNumber \\ 400 = 0 ]]

The above languages do not provide native support.

You need to define global functions or use helpers.

Incorrect Global Approach

PHP (Again):

<?php

$yearNumber = 2024;
$isLeap = checkdate(2, 29, $yearNumber);

R:

leap_year(2024)

Ruby:

year = 2024
is_leap = Date.leap?(year)

Swift:

let yearNumber = 2024
let isLeap = Calendar.current.isDateInLeapYear(
  Date(timeIntervalSince1970: TimeInterval(yearNumber)))

These languages use global functions to check if a year is a leap.

These utility global methods mistakenly place responsibility in the wrong location (a global access point).

Helpers Bad Approach

C#:

int yearNumber = 2024;
bool isLeap = System.DateTime.IsLeapYear(yearNumber);

Dart:

import 'package:intl/intl.dart';
var year = 2024;
var isLeap = DateTime(year).isLeapYear;

Perl:

use Time::Piece;
my $yearNumber = 2024;
my $isLeap = Time::Piece
  ->strptime("$yearNumber-01-01", "%Y-%m-%d")->leapyear;

Python:

import calendar
leap = calendar.isleap(2024)

Visual Basic .NET:

Dim year As Integer = 2024
Dim isLeap As Boolean = DateTime.IsLeapYear(year)

These languages use helpers as libraries to check if a year is a leap.

The misplaced responsibly is not present in a real object but in a bag of DateTime related functions.

The Year Approach

Java:

int yearNumber = 2024;
boolean isLeap = java.time.Year.of(yearNumber).isLeap();

Kotlin:

val yearNumber = 2024
val isLeap = java.time.Year.of(yearNumber).isLeap

Scala:

val year = 2024
val isLeap = java.time.Year.of(year).isLeap

These languages rely on the Year to check if it is a leap.

The protocol is closer to the real world in the bijection

Notice they create Year objects and not Integer objects since this would also break the bijection.

A Year has a different protocol than an integer, and modeling a Year as an integer would also be a premature optimization smell and a symptom of mixing the what and the how.

A Year can tell if it is a leap (an integer shouldn't do it) and can tell you about its months (which are Months, not 0-based integers, 1-based integers or strings).

Conversely, an Integer's capabilities extend to arithmetic operations such as multiplication and exponentiation.

Time is not a joke

Representing a point in time as a float, integer, or any other data type comes with consequences.

You can break a point in time in the real world in tiny fractions (but not too small)

Using floats is not a valid option.

0.01 + 0.02 is not 0.03, and this has terrible consequences dealing with floating point points in time.

The Challenge

We've been talking about leap years.

What are the needs to know if a year is a leap?

The date and time mechanics you model need to know the February 28th, 2024 successor.

But this is NOT your problem.

Following the information hiding principle, you should leave the responsibility as a private protocol.

Conclusion

There is no Silver Bullet.

Use your language wisely.

Today is February 29th, a leap day to pause and reflect on the tools you use daily.

See you in 4 years.