1003 — Leap Year
Tutorial Video
Text description (accessibility)
This video demonstrates the "1003 — Leap Year" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Determine whether a year is a leap year using the Gregorian calendar rules: a year is a leap year if it is divisible by 400, or divisible by 4 but not divisible by 100. Key difference from OCaml: | Aspect | Rust | OCaml |
Tutorial
The Problem
Determine whether a year is a leap year using the Gregorian calendar rules: a year is a leap year if it is divisible by 400, or divisible by 4 but not divisible by 100. Implement is_leap_year(year: u32) -> bool as a single boolean expression and verify on 2000 (leap), 1900 (not), 2004 (leap), 2001 (not).
🎯 Learning Outcomes
% modulo and &&/|| with correct precedence (no extra parentheses needed)(year % 400 == 0) || (year % 4 == 0 && year % 100 != 0) is the canonical formu32 year and % operator to OCaml's int and modCode Example
pub fn is_leap_year(year: u32) -> bool {
(year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)
}
// Usage: is_leap_year(2000)
// Result: trueKey Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Modulo | year % 4 | year mod 4 |
| Equality | == 0 | = 0 |
| Not equal | != 0 | <> 0 |
| Year type | u32 (unsigned) | int (signed) |
| Boolean ops | &&, \|\| | &&, \|\| |
| Branchless | Yes | Yes |
This is one of the cleanest cross-language demonstrations: the algorithm is identical, the syntax differs only in operator spelling. Use it to show that problem-solving translates directly between languages.
OCaml Approach
let leap_year year = (year mod 400 = 0) || (year mod 4 = 0 && year mod 100 <> 0) is the direct translation. The logic is identical; only the syntax differs: mod instead of %, = instead of ==, <> instead of !=. Both implementations are O(1) and branchless.
Full Source
#![allow(clippy::all)]
//! Leap Year Validator
//!
//! A year is a leap year if:
//! - It is divisible by 400, OR
//! - It is divisible by 4 AND not divisible by 100
//!
//! Examples:
//! - 2000: leap (divisible by 400)
//! - 1900: not leap (divisible by 100 but not 400)
//! - 2004: leap (divisible by 4 but not 100)
//! - 2001: not leap (not divisible by 4)
//!
//! Note: We use the `%` operator directly instead of `is_multiple_of()` to match
//! the idiomatic style of the OCaml implementation and because the modulo operator
//! is more familiar to a broader audience of programmers.
/// Determines if a year is a leap year (idiomatic Rust expression).
///
/// # Arguments
/// * `year` - The year to check
///
/// # Returns
/// `true` if the year is a leap year, `false` otherwise
///
/// # Examples
/// ```no_run
/// use leap_year::is_leap_year;
/// assert!(is_leap_year(2000));
/// assert!(!is_leap_year(1900));
/// assert!(is_leap_year(2004));
/// assert!(!is_leap_year(2001));
/// ```
pub fn is_leap_year(year: u32) -> bool {
(year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)
}
/// Determines if a year is a leap year using guard clauses.
///
/// This is an alternative implementation that demonstrates a different
/// coding style using early returns and guard clauses.
///
/// # Arguments
/// * `year` - The year to check
///
/// # Returns
/// `true` if the year is a leap year, `false` otherwise
///
/// # Examples
/// ```no_run
/// use leap_year::is_leap_year_guards;
/// assert!(is_leap_year_guards(2000));
/// assert!(!is_leap_year_guards(1900));
/// ```
pub fn is_leap_year_guards(year: u32) -> bool {
// Divisible by 400 is always a leap year
if year % 400 == 0 {
return true;
}
// Divisible by 100 (but not 400 due to above guard) is never a leap year
if year % 100 == 0 {
return false;
}
// Divisible by 4 is a leap year
year % 4 == 0
}
#[cfg(test)]
mod tests {
use super::*;
// Tests for divisible by 400 (always leap)
#[test]
fn test_divisible_by_400() {
assert!(is_leap_year(2000));
assert!(is_leap_year(2400));
assert!(is_leap_year(1600));
}
// Tests for divisible by 100 but not 400 (never leap)
#[test]
fn test_divisible_by_100_not_400() {
assert!(!is_leap_year(1900));
assert!(!is_leap_year(2100));
assert!(!is_leap_year(1800));
}
// Tests for divisible by 4 but not 100 (always leap)
#[test]
fn test_divisible_by_4_not_100() {
assert!(is_leap_year(2004));
assert!(is_leap_year(2008));
assert!(is_leap_year(2012));
assert!(is_leap_year(2016));
}
// Tests for non-leap years (not divisible by 4)
#[test]
fn test_non_leap_years() {
assert!(!is_leap_year(2001));
assert!(!is_leap_year(2002));
assert!(!is_leap_year(2003));
assert!(!is_leap_year(2017));
}
// Guard clause implementation should match the expression version
#[test]
fn test_both_implementations_match() {
for year in [2000, 1900, 2004, 2001, 1600, 2100, 2008, 2017].iter() {
assert_eq!(
is_leap_year(*year),
is_leap_year_guards(*year),
"Implementations diverged for year {}",
year
);
}
}
// Edge cases
#[test]
fn test_edge_cases() {
assert!(!is_leap_year(1));
assert!(!is_leap_year(3));
assert!(is_leap_year(4));
assert!(is_leap_year(400));
}
}#[cfg(test)]
mod tests {
use super::*;
// Tests for divisible by 400 (always leap)
#[test]
fn test_divisible_by_400() {
assert!(is_leap_year(2000));
assert!(is_leap_year(2400));
assert!(is_leap_year(1600));
}
// Tests for divisible by 100 but not 400 (never leap)
#[test]
fn test_divisible_by_100_not_400() {
assert!(!is_leap_year(1900));
assert!(!is_leap_year(2100));
assert!(!is_leap_year(1800));
}
// Tests for divisible by 4 but not 100 (always leap)
#[test]
fn test_divisible_by_4_not_100() {
assert!(is_leap_year(2004));
assert!(is_leap_year(2008));
assert!(is_leap_year(2012));
assert!(is_leap_year(2016));
}
// Tests for non-leap years (not divisible by 4)
#[test]
fn test_non_leap_years() {
assert!(!is_leap_year(2001));
assert!(!is_leap_year(2002));
assert!(!is_leap_year(2003));
assert!(!is_leap_year(2017));
}
// Guard clause implementation should match the expression version
#[test]
fn test_both_implementations_match() {
for year in [2000, 1900, 2004, 2001, 1600, 2100, 2008, 2017].iter() {
assert_eq!(
is_leap_year(*year),
is_leap_year_guards(*year),
"Implementations diverged for year {}",
year
);
}
}
// Edge cases
#[test]
fn test_edge_cases() {
assert!(!is_leap_year(1));
assert!(!is_leap_year(3));
assert!(is_leap_year(4));
assert!(is_leap_year(400));
}
}
Deep Comparison
Detailed Comparison: OCaml vs Rust — Leap Year Validator
Side-by-Side Code Comparison
OCaml Implementation
let leap_year year =
(year mod 400 = 0) || (year mod 4 = 0 && year mod 100 <> 0)
(* Usage: leap_year 2000 *)
(* Result: true *)
Rust Implementation (Idiomatic)
pub fn is_leap_year(year: u32) -> bool {
(year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)
}
// Usage: is_leap_year(2000)
// Result: true
Rust Implementation (Guard Clauses)
pub fn is_leap_year_guards(year: u32) -> bool {
if year % 400 == 0 {
return true;
}
if year % 100 == 0 {
return false;
}
year % 4 == 0
}
Type Signature Comparison
| Aspect | OCaml | Rust |
|---|---|---|
| Function Definition | let leap_year year = | fn is_leap_year(year: u32) -> bool |
| Parameter Type | Inferred (any numeric type supporting mod) | Explicit: u32 (unsigned 32-bit integer) |
| Return Type | Inferred: bool | Explicit: bool |
| Type Annotation | Optional/Inferred | Required |
| Visibility | Public by default | Requires pub keyword |
| Full Signature | int -> bool | fn(u32) -> bool |
Operator Mapping
| Operation | OCaml | Rust | Semantics |
|---|---|---|---|
| Modulo | mod | % | Remainder after division |
| Equal | = | == | Comparison (not assignment) |
| Not Equal | <> | != | Inequality check |
| Logical AND | && | && | Short-circuit AND |
| Logical OR | \|\| | \|\| | Short-circuit OR |
| Assignment | <- (mutable) | = | Bind/assign value |
Execution Flow Analysis
Leap Year Check: 2000
OCaml:
(2000 mod 400 = 0) || (2000 mod 4 = 0 && 2000 mod 100 <> 0)
↓
(0 = 0) || (...)
↓
true || (...) ← Short-circuit: stops here
↓
true
Rust:
(2000 % 400 == 0) || (2000 % 4 == 0 && 2000 % 100 != 0)
↓
(0 == 0) || (...)
↓
true || (...) ← Short-circuit: stops here
↓
true
Leap Year Check: 1900
OCaml:
(1900 mod 400 = 0) || (1900 mod 4 = 0 && 1900 mod 100 <> 0)
↓
(300 = 0) || (0 = 0 && 0 <> 0)
↓
false || (true && false)
↓
false || false
↓
false
Rust:
(1900 % 400 == 0) || (1900 % 4 == 0 && 1900 % 100 != 0)
↓
(300 == 0) || (0 == 0 && 0 != 0)
↓
false || (true && false)
↓
false || false
↓
false
Memory and Performance Considerations
| Aspect | OCaml | Rust |
|---|---|---|
| Integer Size | 63-bit signed (on 64-bit systems) | 32-bit unsigned (u32) or specified type |
| Stack Allocation | Automatic | On stack (no allocation needed) |
| Register Usage | Likely one register per operation | Single register, highly optimized |
| Generated Code | 3-4 mod operations + 2 comparisons | 3-4 mod operations + 2 comparisons |
| Optimization | OCaml compiler optimizes well | LLVM backend optimizes aggressively |
| Inlining | Inline if called from optimized code | Always inlined in release builds |
Testing Approach
OCaml
let () = assert (leap_year 2000 = true)
let () = assert (leap_year 1900 = false)
let () = assert (leap_year 2004 = true)
let () = assert (leap_year 2001 = false)
Characteristics:
Assert_failure exceptionRust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divisible_by_400() {
assert!(is_leap_year(2000));
}
#[test]
fn test_divisible_by_100_not_400() {
assert!(!is_leap_year(1900));
}
}
Characteristics:
#[test] attribute for test functionstests modulecargo test commandError Handling
OCaml Behavior
mod operator works with negative numbers - Example: (-5) mod 3 = (-2) (follows divisor sign)
Rust Behavior
u32 prevents negative input at type level% with u32 always produces non-negative resultu32Documentation
OCaml
(** Determines if a year is a leap year.
@param year the year to check
@return true if the year is a leap year, false otherwise
*)
let leap_year year = ...
Tool: OCamldoc or plain comments
Rust
/// Determines if a year is a leap year.
///
/// # Arguments
/// * `year` - The year to check
///
/// # Returns
/// `true` if the year is a leap year, `false` otherwise
pub fn is_leap_year(year: u32) -> bool { ... }
Tool: rustdoc (integrated into cargo)
5 Key Insights
1. Operator Similarity Masks Type System Differences
At first glance, the code looks nearly identical:
(year mod 400 = 0) || (year mod 4 = 0 && year mod 100 <> 0) // OCaml
(year % 400 == 0) || (year % 4 == 0 && year % 100 != 0) // Rust
But beneath the surface:
year could be any numeric type; mod is polymorphicyear must be u32 (or explicitly annotated); no ambiguity2. Integer Type Matters More in Rust
In Rust, choosing u32 vs i32 vs u64 has real consequences:
u32** (unsigned 32-bit): Years fit comfortably; prevents negative numbersi32** (signed 32-bit): Allows negative years but uses extra bit for signu64** (unsigned 64-bit): Overkill for years, wastes spaceIn OCaml, int is polymorphic and the type checker infers the best fit. Rust forces the programmer to decide, which can feel tedious but is more explicit.
3. Short-Circuit Evaluation Is Critical
Both languages support short-circuit evaluation:
(year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)
This means:
year % 400 == 0 is true, the rest of the expression is never evaluatedWithout short-circuit evaluation, we'd always compute 3 modulo operations (wasteful).
4. Guard Clauses Trade Brevity for Clarity
The guard clause version is longer but potentially clearer:
if year % 400 == 0 { return true; } // "First check this"
if year % 100 == 0 { return false; } // "Then check this"
year % 4 == 0 // "Finally check this"
This reads more like natural language ("if this, return true; else if that, return false").
The expression version is more compact but requires the reader to understand operator precedence and associativity.
5. Rust's Explicit Return Patterns Enable Refactoring
In the guard clause version:
if year % 400 == 0 { return true; }
If you later want to add logging or side effects:
if year % 400 == 0 {
println!("Year {} is leap (divisible by 400)", year);
return true;
}
In the expression version, adding side effects would require restructuring:
pub fn is_leap_year(year: u32) -> bool {
let divisible_by_400 = year % 400 == 0;
if divisible_by_400 {
println!("Year {} is leap (divisible by 400)", year);
}
divisible_by_400 || (year % 4 == 0 && year % 100 != 0)
}
This is one reason why guard clauses are sometimes preferred in larger functions: they give you a "handle" to insert imperative code.
Summary Table
| Metric | OCaml | Rust |
|---|---|---|
| Lines of Code | 1 | 1 (expression) / 6 (guards) |
| Type Inference | Complete | Partial (parameters explicit) |
| Integer Type | Inferred | Explicit (u32) |
| Documentation | OCamldoc-style | Rustdoc (integrated) |
| Testing | Ad-hoc assertions | Built-in test framework |
| Compilation | Bytecode or native | LLVM IR to native |
| Execution Speed | Fast (native compiler) | Very fast (LLVM optimization) |
| Refactoring Ease | Easy (simple expression) | Moderate (type constraints) |
| Maintainability | High (concise, clear) | High (explicit, safe) |
Both implementations are correct, safe, and efficient. The choice between them depends on your project's priorities: OCaml favors brevity and inference, while Rust favors explicitness and safety guarantees.
Exercises
match on (year % 400, year % 100, year % 4) tuples and compare readability.days_in_year(year: u32) -> u32 that returns 366 for leap years and 365 for non-leap years.next_leap_year(year: u32) -> u32 that finds the first leap year strictly after the given year.(1..=2100).filter(|&y| is_leap_year(y)).count().leap_years_between : int -> int -> int list that returns all leap years in the inclusive range.