1122-leap-year — Leap Year
Tutorial
The Problem
A leap year in the Gregorian calendar occurs every 4 years, except for centuries (divisible by 100), which are only leap years if also divisible by 400. The rule has three conditions arranged in a specific priority: (divisible by 4) AND NOT (divisible by 100) OR (divisible by 400). This is a classic exercise in boolean logic and is the first date-calculation building block.
The Gregorian calendar reform in 1582 introduced this rule to keep the calendar aligned with the solar year (365.2425 days). Software that handles dates — from scheduling systems to financial calculations — must implement this rule correctly.
🎯 Learning Outcomes
Code Example
pub fn is_leap_year(year: i32) -> bool {
year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}Key Differences
&& binds tighter than || in Rust and OCaml, making the formula a || (b && c) without explicit parentheses — but adding them improves clarity.% / mod) for the divisibility checks; negative years behave differently in each language's modulo semantics.chrono crate**: Production Rust uses the chrono crate for date calculations; OCaml uses Calendar or Ptime.chrono handles this, raw implementations often do not.OCaml Approach
let is_leap_year year =
year mod 400 = 0 || (year mod 4 = 0 && year mod 100 <> 0)
Identical logic. OCaml uses mod instead of % and <> instead of != for not-equal, but the boolean structure is the same.
Full Source
#![allow(dead_code)]
//! Leap Year — Gregorian calendar rule
//! See example.ml for OCaml reference
/// Returns true if `year` is a leap year in the Gregorian calendar.
/// Rule: divisible by 400, OR (divisible by 4 AND NOT divisible by 100).
///
/// Mirrors OCaml:
/// `year mod 400 = 0 || (year mod 4 = 0 && year mod 100 <> 0)`
pub fn is_leap_year(year: i32) -> bool {
year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}
/// Functional/recursive variant: decompose the rule into named predicates.
/// Shows the three-part logic in a way that mirrors the Gregorian definition.
pub fn is_leap_year_explicit(year: i32) -> bool {
let divisible_by_4 = year % 4 == 0;
let divisible_by_100 = year % 100 == 0;
let divisible_by_400 = year % 400 == 0;
divisible_by_400 || (divisible_by_4 && !divisible_by_100)
}
/// Number of days in a year — 366 for leap years, 365 otherwise.
pub fn days_in_year(year: i32) -> u32 {
if is_leap_year(year) {
366
} else {
365
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regular_non_leap() {
// Not divisible by 4
assert!(!is_leap_year(1997));
assert!(!is_leap_year(2001));
}
#[test]
fn test_regular_leap() {
// Divisible by 4, not by 100
assert!(is_leap_year(1996));
assert!(is_leap_year(2004));
assert!(is_leap_year(2024));
}
#[test]
fn test_century_not_leap() {
// Divisible by 100 but not 400
assert!(!is_leap_year(1900));
assert!(!is_leap_year(1800));
assert!(!is_leap_year(2100));
}
#[test]
fn test_century_leap() {
// Divisible by 400
assert!(is_leap_year(1600));
assert!(is_leap_year(2000));
assert!(is_leap_year(2400));
}
#[test]
fn test_explicit_matches_idiomatic() {
for year in [1900, 1996, 2000, 2001, 2024, 2100] {
assert_eq!(is_leap_year(year), is_leap_year_explicit(year));
}
}
#[test]
fn test_days_in_year() {
assert_eq!(days_in_year(2000), 366);
assert_eq!(days_in_year(1900), 365);
assert_eq!(days_in_year(2024), 366);
assert_eq!(days_in_year(2023), 365);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regular_non_leap() {
// Not divisible by 4
assert!(!is_leap_year(1997));
assert!(!is_leap_year(2001));
}
#[test]
fn test_regular_leap() {
// Divisible by 4, not by 100
assert!(is_leap_year(1996));
assert!(is_leap_year(2004));
assert!(is_leap_year(2024));
}
#[test]
fn test_century_not_leap() {
// Divisible by 100 but not 400
assert!(!is_leap_year(1900));
assert!(!is_leap_year(1800));
assert!(!is_leap_year(2100));
}
#[test]
fn test_century_leap() {
// Divisible by 400
assert!(is_leap_year(1600));
assert!(is_leap_year(2000));
assert!(is_leap_year(2400));
}
#[test]
fn test_explicit_matches_idiomatic() {
for year in [1900, 1996, 2000, 2001, 2024, 2100] {
assert_eq!(is_leap_year(year), is_leap_year_explicit(year));
}
}
#[test]
fn test_days_in_year() {
assert_eq!(days_in_year(2000), 366);
assert_eq!(days_in_year(1900), 365);
assert_eq!(days_in_year(2024), 366);
assert_eq!(days_in_year(2023), 365);
}
}
Deep Comparison
OCaml vs Rust: Leap Year
Side-by-Side Code
OCaml
let is_leap_year year =
(year mod 400 = 0) ||
(year mod 4 = 0 && year mod 100 <> 0)
let () =
assert (is_leap_year 2000 = true);
assert (is_leap_year 1900 = false);
assert (is_leap_year 2024 = true);
print_endline "ok"
Rust (idiomatic)
pub fn is_leap_year(year: i32) -> bool {
year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
}
Rust (explicit decomposition)
pub fn is_leap_year_explicit(year: i32) -> bool {
let divisible_by_4 = year % 4 == 0;
let divisible_by_100 = year % 100 == 0;
let divisible_by_400 = year % 400 == 0;
divisible_by_400 || (divisible_by_4 && !divisible_by_100)
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Function signature | val is_leap_year : int -> bool | fn is_leap_year(year: i32) -> bool |
| Integer type | int (63-bit on 64-bit systems) | i32 (explicit 32-bit signed) |
| Boolean operators | &&, \|\|, not | &&, \|\|, ! |
| Modulus operator | mod (keyword) | % (operator) |
| Inequality | <> | != |
Key Insights
mod as an infix keyword; Rust uses % as an operator — both compute the remainder.divisible_by_4, etc.) — OCaml supports this too via let bindings, but the one-liner is idiomatic in both.int is machine-width (63-bit on 64-bit); Rust forces you to choose i32 or i64 explicitly, which matters for domain modeling.When to Use Each Style
Use the one-liner Rust version when: the logic is clear in a single line and readability is not compromised; idiomatic for boolean predicates. Use the explicit decomposition when: the rule has three or more named parts and clarity is paramount; especially useful in code review or educational contexts.
Exercises
days_in_month(year: i32, month: u32) -> u32 using is_leap_year for February.day_of_year(year: i32, month: u32, day: u32) -> u32 that returns the ordinal day (1–365/366).quickcheck that verifies is_leap_year matches the chrono crate's answer for all years from 1 to 9999.