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

Maxi Contieri - Feb 29 - - Dev Community

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));
Enter fullscreen mode Exit fullscreen mode

SQL (PostgreSQL):

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

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;
Enter fullscreen mode Exit fullscreen mode

C/C++:

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

Go:

package main

import (
  "fmt"
  "time"
)

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

Haskell:

import Data.Time.Calendar (isLeapYear)
let year = 2024
let isLeap = isLeapYear year
Enter fullscreen mode Exit fullscreen mode

JavaScript/TypeScript:

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

Julia:

using Dates
year = 2024
isleap(year)
Enter fullscreen mode Exit fullscreen mode

Lua:

local year = 2024
local isLeap = (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)
Enter fullscreen mode Exit fullscreen mode

MATLAB:

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

Objective-C:

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

PowerShell:

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

Rust:

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

Smalltalk:

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

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);
Enter fullscreen mode Exit fullscreen mode

R:

leap_year(2024)
Enter fullscreen mode Exit fullscreen mode

Ruby:

year = 2024
is_leap = Date.leap?(year)
Enter fullscreen mode Exit fullscreen mode

Swift:

let yearNumber = 2024
let isLeap = Calendar.current.isDateInLeapYear(
  Date(timeIntervalSince1970: TimeInterval(yearNumber)))
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Dart:

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

Perl:

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

Python:

import calendar
leap = calendar.isleap(2024)
Enter fullscreen mode Exit fullscreen mode

Visual Basic .NET:

Dim year As Integer = 2024
Dim isLeap As Boolean = DateTime.IsLeapYear(year)
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Kotlin:

val yearNumber = 2024
val isLeap = java.time.Year.of(yearNumber).isLeap
Enter fullscreen mode Exit fullscreen mode

Scala:

val year = 2024
val isLeap = java.time.Year.of(year).isLeap
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .