ExamplesBy LevelBy TopicLearning Paths
1122 Fundamental

1122-leap-year — Leap Year

Functional Programming

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

  • • Implement the three-part leap year rule correctly
  • • Express the rule as a boolean formula with correct operator precedence
  • • Write comprehensive tests covering all four cases (regular, century, leap century, non-leap)
  • • Understand why the rule exists (solar year alignment)
  • • Handle edge cases: year 0 (proleptic Gregorian), negative years
  • Code Example

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

    Key Differences

  • Operator precedence: Both && binds tighter than || in Rust and OCaml, making the formula a || (b && c) without explicit parentheses — but adding them improves clarity.
  • Integer division: Both use remainder (% / 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.
  • Calendar systems: The Gregorian rule applies to years > 1582; proleptic Gregorian extends it backward — 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Function signatureval is_leap_year : int -> boolfn is_leap_year(year: i32) -> bool
    Integer typeint (63-bit on 64-bit systems)i32 (explicit 32-bit signed)
    Boolean operators&&, \|\|, not&&, \|\|, !
    Modulus operatormod (keyword)% (operator)
    Inequality<>!=

    Key Insights

  • Operator vs. keyword: OCaml uses mod as an infix keyword; Rust uses % as an operator — both compute the remainder.
  • Operator precedence: The parenthesization is the same in both languages: the OR has lower precedence than AND; both require explicit parentheses to clarify the three-part rule.
  • Naming booleans explicitly: The "explicit" Rust version names each condition (divisible_by_4, etc.) — OCaml supports this too via let bindings, but the one-liner is idiomatic in both.
  • Type width: OCaml's int is machine-width (63-bit on 64-bit); Rust forces you to choose i32 or i64 explicitly, which matters for domain modeling.
  • Pure function: Both implementations are pure — the same input always gives the same output, with no side effects. This enables easy unit testing and reasoning.
  • 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

  • Write days_in_month(year: i32, month: u32) -> u32 using is_leap_year for February.
  • Implement day_of_year(year: i32, month: u32, day: u32) -> u32 that returns the ordinal day (1–365/366).
  • Write a property-based test using quickcheck that verifies is_leap_year matches the chrono crate's answer for all years from 1 to 9999.
  • Open Source Repos