leapc
Loading...
Searching...
No Matches
Leap Years in C for Embedded Systems

This repo delves into the intricacies of leap years, exploring their historical context, the rules governing their occurrence, and practical methods for calculating them in embedded systems. We discuss the significance of leap years in maintaining calendar accuracy and provide efficient algorithms suitable for resource-constrained environments. The article also highlights common pitfalls and best practices for implementing leap year calculations in C programming, ensuring reliability and precision in timekeeping applications.

The Problem

What is a leap year? In simple mathematical terms,

$$ f_{leap}(year) = ( year \bmod 4 = 0 ) \land \left[ (year \bmod 100 \neq 0) \lor (year \bmod 400 = 0) \right] $$

where $x\bmod y$ is the integer “modulo” operation: the remainder after dividing a numerator by a denominator; the modulo operation computes the “modulus.”

Developing a super-simple library for manipulating Gregorian epoch times that account for leap years is the goal of this article. Such a library has no low-level dependencies. It depends only on raw C, making it suitable for embedded systems.

Does a Year Leap?

In basic C, the is_leap function answers true or false:

#include <stdbool.h>
static inline bool is_leap(int year) {
/*
* Allow C99's standard precedence to rule over operator ordering: modulo
* exceeds equality and inequality operators.
*/
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}
bool is_leap(int year)
Determine if a year is a leap year.
Definition leap.c:13

Notice that $year$ exists in integer space: $year\in\mathbb{Z}$. This is by design and not just for performance. A year is a whole number for the purpose of leap determination. For contemporary embedded systems, an integer is typically a signed 32-bit number; this provides more than enough resolution for epoch years, which would normally fall between $1970$ and $2025$, the current year.

The Solution

Computing leap years through a range requires a specialised quotient-modulo function that handles negative divisors correctly. The following sections expand on this issue and walk through the complete implementation.

Leap Through the Years

Another way of looking at the leap year formula exists. For some $year$, the following sum gives the total number of “leap years” through the range $thru_{leap}\in\left[0, year\right)$.

$$ thru_{leap}\in\left[0, year\right)=f_{quo}(year, 4) - f_{quo}(year, 100) + f_{quo}(year, 400) $$

Note carefully: the exclusive upper boundary. This is important. The sum does not include the given year; it includes all the years up to the year, but not the year itself. The $f_{quo}$ function computes the integer quotient of its two arguments.

Quotient and modulus

The $f_{quo}(x, y)$ function amounts to, in C:

struct quo_mod {
int quo;
int mod;
};
static inline struct quo_mod quo_mod(int x, int y) {
/*
* Compute modulus using C's % operator. Note that C's % operator will yield
* negative results when the numerator is negative.
*/
int mod = x % y;
/* Adjust negative modulus to be positive. Ensures that:
*
* 0 <= mod < y when y > 0
* y < mod <= 0 when y < 0
*
* This matches Lua's modulo operator behaviour.
*/
if (mod != 0 && (mod ^ y) < 0) {
mod += y;
}
/*
* Returns a quo_mod structure by casting an initialiser. Is this portable?
*/
return (struct quo_mod){.quo = (x - mod) / y, .mod = mod};
}
Quotient and remainder in integer space.
Definition quo_mod.h:22
int mod
Integer modulus.
Definition quo_mod.h:30
int quo
Integer quotient.
Definition quo_mod.h:26

The implementation tracks Lua’s module operator. The sign differs. In C, the sign of the modulus follows the sign of the numerator, but for Lua, it follows the sign of the divisor. The C implementation needs a small correction; add the divisor if the signs differ between the modulus and the divisor.

$$ \begin{aligned} m' &= x \bmod y \ m &= \begin{cases} m' + y & \text{if } m' \neq 0 \text{ and } \text{sgn}(m') \neq \text{sgn}(y) \ m' & \text{otherwise} \end{cases} \ q &= \frac{x - m}{y} \end{aligned} $$

Properties include:

$$ \begin{aligned} 0 \leq m < y & \quad \text{when } y > 0 \ y < m \leq 0 & \quad \text{when } y < 0 \end{aligned} $$

And the invariant $x = y \cdot q + m$ applies. The simple reason for this disparity between Lua and C boils down to the fact that Lua uses only floating-point numbers at version $5.1$. For that reason, it computes % in $\mathbb{R}$ space using division and “floor” as follows.

$$ \text{luai}_{nummod}(a, b) = a - \left\lfloor \frac{a}{b} \right\rfloor \cdot b $$

Leap through implementation in C

The “leap years through some year” function in C amounts to a simple sum of quotients.

static inline int leap_thru(int year) {
/*
* Expand the quotient terms first for debugging. Make it easier to see the
* terms of the thru-sum.
*/
const int q4 = quo_mod(year, 4).quo;
const int q100 = quo_mod(year, 100).quo;
const int q400 = quo_mod(year, 400).quo;
return q4 - q100 + q400;
}
int leap_thru(int year)
Leap years completed from year 0 up to but not including the first day of the specified year.
Definition leap.c:30

The implementation expands the quotient terms for clarity; they make debugging easier since each term appears separately in the debugger. The following unit tests verify the implementation.

#include "leap.h"
#include <assert.h>
void leap_thru_test(void) {
assert(leap_thru(0) == 0);
assert(leap_thru(1) == 0);
assert(leap_thru(2) == 0);
assert(leap_thru(3) == 0);
assert(leap_thru(4) == 1);
assert(leap_thru(5) == 1);
assert(leap_thru(100) == 24);
assert(leap_thru(101) == 24);
assert(leap_thru(200) == 48);
assert(leap_thru(201) == 48);
assert(leap_thru(400) == 97);
assert(leap_thru(401) == 97);
}
Leap year function prototypes.

Leap-Adjusted Days and Offset

Now that the leap library can compute the number of leap years in the range $[0,year)$, the number of days in that range becomes readily derived. Start by assuming $365$ days per year without adjustment. Apply the leap-through adjustment for all the preceding years. The result is the number of epoch days starting at year $0$ and ending at the given $year$.

static inline int leap_day(int year) {
return year * 365 + leap_thru(year - 1) + 1;
}
int leap_day(int year)
Counts leap-adjusted days up to some year.
Definition leap.c:48

The $+1$ in leap_day anchors the epoch so that the start of year $0$ maps to day $0$ while still counting year $0$ as a leap year. The closed-form term year * 365 + leap_thru(year - 1) counts $365$-day years plus leap days up to (but not including) the target year; because year $0$ itself is a leap year in the proleptically[^1] applied Gregorian model, that formula alone would put leap_day(0) at $-1$ and undercount every subsequent absolute day number by one. Adding $1$ fixes the baseline: leap_day(0) becomes $0$, leap_day(1) becomes $366$[^2] and differences between years remain correct because the constant offsets cancel in subtractions.

Leap offset

The leap_off function (see implementation below) normalises an arbitrary day offset relative to a given year into a canonical $(year, day_{of_{year}})$ pair where $0 \le day_{of_{year}} < days_{in_{year}}$. Its algorithm works as follows:

  • Compute the current year’s length: days = 365 + leap_add(year).
  • While $day$ lies outside $[0, days)$:
    • Jump whole years using floor-like quotient semantics: year0 = year + quo_mod(day, days).quo.
    • Rebase the offset to the new year using absolute day counts: day += leap_day(year) - leap_day(year0).
    • Update year to year0 and recompute days for that year.
  • Return the resulting (struct leap_off){year, day} which is within the year’s bounds.

Notes:

  • quo_mod uses Lua-style modulo semantics, so negative offsets jump the correct number of whole years in the negative direction.
  • Differences of leap_day(year) cancel the constant epoch offset, ensuring exact rebasing regardless of the $+1$ anchor in leap_day.
struct leap_off {
int year;
int day;
};
static inline struct leap_off leap_off(int year, int day_off) {
int days = 365 + leap_add(year);
while (day_off < 0 || day_off >= days) {
int year0 = year + quo_mod(day_off, days).quo;
day_off += leap_day(year) - leap_day(year0);
days = 365 + leap_add(year = year0);
}
return (struct leap_off){.year = year, .day = day_off};
}
int leap_add(int year)
Adds one for a leap year otherwise zero.
Definition leap.c:28
Leap offset by year and day.
Definition leap.h:77
int day
Day of year offset.
Definition leap.h:85
int year
Year offset.
Definition leap.h:81

Notice that the day_off parameter is zero-based: the first day of the year is day $0$. This argument is a day offset relative to some year, not a one-based day of the month. This is an important distinction. The day is a zero-based offset in days compared to some base year. The function returns a struct leap_off containing the adjusted year and day; also an offset.

This is a key function for manipulating epoch days in embedded systems where no standard library exists. It allows day offsets to be normalised into year-and-day pairs that embedded applications can use for date calculations. Relative leap-year computations become straightforward, as in the following example.

/*
* Negative day offset that normalises to the previous year.
* Year 5 offset -1 day normalises to year 4 day 365 (leap year).
*/
assert(equal_leap_off((struct leap_off){4, 365}, leap_off(5, -1)));
static bool equal_leap_off(struct leap_off lhs, struct leap_off rhs)
Compares two leap_off structures for equality.
Definition leap.h:105

Month and Year Day

Given some $year$ and some $1\le month\le12$ ordinal, computing the days in the month or the days of the year for that month becomes straightforward, and a date (year, month ordinal, day of month ordinal) from a year-day offset conversion falls out.

int leap_mday(int year, int month) {
static const int MDAY[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
const struct quo_mod qm = quo_mod(month - 1, 12);
return MDAY[qm.mod] + (qm.mod == 1 ? leap_add(year + qm.quo) : 0);
}
int leap_yday(int year, int month) {
static const int YDAY[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
const struct quo_mod qm = quo_mod(month - 1, 12);
return YDAY[qm.mod] + (qm.mod > 1 ? leap_add(year + qm.quo) : 0);
}
int leap_mday(int year, int month)
Day of month from year and month.
Definition leap.c:80
int leap_yday(int year, int month)
Day of year from year and month.
Definition leap.c:86

Leap Date

The year-day offset to date normalisation function applies the following algorithm for Converting a (year, day-of-year cardinal offset) pair into a (year, month ordinal, day-of-month ordinal) triple.

  • Normalise the (year, day) pair using leap_off to ensure day is within the year’s bounds.
  • Iterate months from 1 to 12, subtracting the number of days in each month from day until day is less than the number of days in the current month.
  • The current month is the target month, and day + 1 is the target day of the month (to convert from 0-based to 1-based).
struct leap_date {
int year;
int month;
int day;
};
static inline struct leap_date leap_date(int year, int day_off) {
struct leap_off off = leap_off(year, day_off);
int month = 1;
for (; month <= 12; ++month) {
const int mday = leap_mday(off.year, month);
if (off.day < mday) {
break;
}
off.day -= mday;
}
return (struct leap_date){
.year = off.year,
.month = month,
.day = off.day + 1,
};
}
Leap year date structure.
Definition leap.h:163
int year
Year.
Definition leap.h:167
int month
Month of year starting from 1 for January.
Definition leap.h:174
int day
Day of month starting from 1 for the first day of the month.
Definition leap.h:181

We can now translate year-day pairs to year-month-day triples.

/*
* Three hundred and sixty five days from midnight on 1900-01-01 is 1900-12-31
* since 1900 is not a leap year.
*/
assert(equal_leap_date((struct leap_date){1900, 12, 31}, leap_date(1900, 364)));
static bool equal_leap_date(struct leap_date lhs, struct leap_date rhs)
Compares two leap_date structures for equality.
Definition leap.h:193

Conclusions

The leapc (leap years in C) mini-library provides a lightweight, self-contained implementation of leap year calculations suitable for embedded systems with minimal dependencies. The implementation is fast and portable, relying only on basic integer arithmetic and division—no heavy floating-point operations or external libraries required. This makes it ideal for embedded firmware deployments where code footprint and computational efficiency matter.

Day-Level Resolution

The library operates at day-level granularity, which covers most calendar applications. Extending to timestamps is straightforward: divide a time value in seconds by $86400$ (seconds per day) to obtain a quotient and remainder. The remainder gives the time-of-day (seconds since midnight), and the quotient represents the epoch day.

To convert to a specific epoch—such as Unix time (1970-01-01) just subtract leap_day(1970) from the quotient. This flexibility is a key strength: the library itself is epoch-agnostic. Callers can anchor to any convenient year: leap_day(1900) for the Gregorian epoch, leap_day(2000) for Windows NT time, or any other reference point. This design avoids baking a single epoch assumption into the core algorithms.

Scope and Future Work

The implementation handles positive years and works well for contemporary applications spanning 1970 through 9999. It assumes the proleptically-extended Gregorian calendar (applying modern leap rules backward to year 0). Handling negative years (B.C. dates) or extending beyond year 9999 would require minor adjustments but is not addressed in this version.

The complete implementation, test suite, and usage examples are available on GitHub. For embedded developers seeking a minimal, efficient leap year library, leapc offers a practical solution.

The library has limitations: it does not handle negative B.C. years. Nor does it address timezone considerations.

[^1]: The computations apply the Gregorian model back to Julian time. Pope Gregory XIII’s issued the papal bull in 1582.

[^2]: $365$ regular days plus the leap day of year $0$