ExamplesBy LevelBy TopicLearning Paths
1003 Fundamental

1003 — Leap Year

Functional Programming

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

  • • Express the Gregorian leap year rule as a compact boolean expression
  • • Use % modulo and &&/|| with correct precedence (no extra parentheses needed)
  • • Write tests covering all four rule branches: 400-divisible, 100-divisible, 4-divisible, none
  • • Understand that (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0) is the canonical form
  • • Map Rust's u32 year and % operator to OCaml's int and mod
  • • Recognise that clean single-expression logic eliminates branching error
  • Code Example

    pub fn is_leap_year(year: u32) -> bool {
        (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)
    }
    
    // Usage: is_leap_year(2000)
    // Result: true

    Key Differences

    AspectRustOCaml
    Moduloyear % 4year mod 4
    Equality== 0= 0
    Not equal!= 0<> 0
    Year typeu32 (unsigned)int (signed)
    Boolean ops&&, \|\|&&, \|\|
    BranchlessYesYes

    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));
        }
    }
    ✓ Tests Rust test suite
    #[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

    AspectOCamlRust
    Function Definitionlet leap_year year =fn is_leap_year(year: u32) -> bool
    Parameter TypeInferred (any numeric type supporting mod)Explicit: u32 (unsigned 32-bit integer)
    Return TypeInferred: boolExplicit: bool
    Type AnnotationOptional/InferredRequired
    VisibilityPublic by defaultRequires pub keyword
    Full Signatureint -> boolfn(u32) -> bool

    Operator Mapping

    OperationOCamlRustSemantics
    Modulomod%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

    AspectOCamlRust
    Integer Size63-bit signed (on 64-bit systems)32-bit unsigned (u32) or specified type
    Stack AllocationAutomaticOn stack (no allocation needed)
    Register UsageLikely one register per operationSingle register, highly optimized
    Generated Code3-4 mod operations + 2 comparisons3-4 mod operations + 2 comparisons
    OptimizationOCaml compiler optimizes wellLLVM backend optimizes aggressively
    InliningInline if called from optimized codeAlways 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:

  • • Uses assertions in module scope
  • • Tests executed at module initialization
  • • No structured test framework
  • • Failed assertions raise Assert_failure exception
  • Rust

    #[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:

  • • Uses #[test] attribute for test functions
  • • Organized in tests module
  • • Run with cargo test command
  • • Includes test output and failure details
  • • Can be built/run separately from main code
  • Error Handling

    OCaml Behavior

  • Negative numbers: mod operator works with negative numbers
  • - Example: (-5) mod 3 = (-2) (follows divisor sign)

  • Type errors: Caught at compile time (strict type checking)
  • Runtime errors: Exceptions raised (e.g., Division by zero)
  • Rust Behavior

  • Unsigned integers: u32 prevents negative input at type level
  • Unsigned mod: % with u32 always produces non-negative result
  • Type errors: Caught at compile time (strict type checking)
  • Panics: Division by zero panics; prevented at type level for u32
  • Documentation

    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:

  • OCaml: year could be any numeric type; mod is polymorphic
  • Rust: year must be u32 (or explicitly annotated); no ambiguity
  • Impact: Rust catches type errors earlier; OCaml is more flexible but requires inference
  • 2. 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 numbers
  • • **i32** (signed 32-bit): Allows negative years but uses extra bit for sign
  • • **u64** (unsigned 64-bit): Overkill for years, wastes space
  • In 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:

  • • If year % 400 == 0 is true, the rest of the expression is never evaluated
  • • This saves 2-3 modulo operations on average
  • • Both languages implement this identically
  • Without 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

    MetricOCamlRust
    Lines of Code11 (expression) / 6 (guards)
    Type InferenceCompletePartial (parameters explicit)
    Integer TypeInferredExplicit (u32)
    DocumentationOCamldoc-styleRustdoc (integrated)
    TestingAd-hoc assertionsBuilt-in test framework
    CompilationBytecode or nativeLLVM IR to native
    Execution SpeedFast (native compiler)Very fast (LLVM optimization)
    Refactoring EaseEasy (simple expression)Moderate (type constraints)
    MaintainabilityHigh (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

  • Write a version using a match on (year % 400, year % 100, year % 4) tuples and compare readability.
  • Implement days_in_year(year: u32) -> u32 that returns 366 for leap years and 365 for non-leap years.
  • Write next_leap_year(year: u32) -> u32 that finds the first leap year strictly after the given year.
  • Count leap years in a range using (1..=2100).filter(|&y| is_leap_year(y)).count().
  • In OCaml, implement leap_years_between : int -> int -> int list that returns all leap years in the inclusive range.
  • Open Source Repos